src/extensions/default/PhpTooling/unittests.js
/*
* Copyright (c) 2019 - present Adobe. 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, runs, beforeEach, it, expect, waitsFor, waitsForDone, beforeFirst, afterLast */
define(function (require, exports, module) {
'use strict';
var SpecRunnerUtils = brackets.getModule("spec/SpecRunnerUtils"),
Strings = brackets.getModule("strings"),
FileUtils = brackets.getModule("file/FileUtils"),
StringUtils = brackets.getModule("utils/StringUtils"),
StringMatch = brackets.getModule("utils/StringMatch");
var extensionRequire,
phpToolingExtension,
testWindow,
$,
PreferencesManager,
CodeInspection,
DefaultProviders,
CodeHintsProvider,
SymbolProviders,
EditorManager,
testEditor,
testFolder = FileUtils.getNativeModuleDirectoryPath(module) + "/unittest-files/",
testFile1 = "test1.php",
testFile2 = "test2.php",
testFile4 = "test4.php";
describe("PhpTooling", function () {
beforeFirst(function () {
// Create a new window that will be shared by ALL tests in this spec.
SpecRunnerUtils.createTestWindowAndRun(this, function (w) {
testWindow = w;
$ = testWindow.$;
var brackets = testWindow.brackets;
extensionRequire = brackets.test.ExtensionLoader.getRequireContextForExtension("PhpTooling");
phpToolingExtension = extensionRequire("main");
});
});
afterLast(function () {
waitsForDone(phpToolingExtension.getClient().stop(), "stoping php server");
testEditor = null;
testWindow = null;
brackets = null;
EditorManager = null;
SpecRunnerUtils.closeTestWindow();
});
beforeEach(function () {
EditorManager = testWindow.brackets.test.EditorManager;
PreferencesManager = testWindow.brackets.test.PreferencesManager;
CodeInspection = testWindow.brackets.test.CodeInspection;
CodeInspection.toggleEnabled(true);
DefaultProviders = testWindow.brackets.getModule("languageTools/DefaultProviders");
CodeHintsProvider = extensionRequire("CodeHintsProvider");
SymbolProviders = extensionRequire("PHPSymbolProviders").SymbolProviders;
});
/**
* Does a busy wait for a given number of milliseconds
* @param {Number} milliSeconds - number of milliSeconds to wait
*/
function waitForMilliSeconds(milliSeconds) {
var flag = false;
setTimeout(function () {
flag = true;
}, milliSeconds);
waitsFor(function () {
return flag;
}, "This should not fail. Please check the timeout values.",
milliSeconds + 10); // We give 10 milliSeconds as grace period
}
/**
* Check the presence of a Button in Error Prompt
* @param {String} btnId - "CANCEL" or "OPEN"
*/
function checkPopUpButton(clickbtnId) {
var doc = $(testWindow.document),
errorPopUp = doc.find(".error-dialog.instance"),
btn = errorPopUp.find('.dialog-button');
// Test if the update bar button has been displayed.
expect(btn.length).toBe(2);
if (clickbtnId) {
clickButton(clickbtnId);
}
}
/**
* Check the presence of a Button in Error Prompt
* @param {String} btnId - Button OPEN or Cancel Button
*/
function clickButton(btnId) {
var doc = $(testWindow.document),
errorPopUp = doc.find(".error-dialog.instance"),
btn = errorPopUp.find('.dialog-button'),
openBtn,
cancelBtn,
clickBtn;
if (btn[0].classList.contains("primary")) {
openBtn = btn[0];
cancelBtn = btn[1];
}
if (btn[1].classList.contains("primary")) {
openBtn = btn[1];
cancelBtn = btn[0];
}
clickBtn = cancelBtn;
if(btnId === "OPEN") {
clickBtn = openBtn;
}
if(clickBtn) {
clickBtn.click();
waitForMilliSeconds(3000);
runs(function() {
expect(doc.find(".error-dialog.instance").length).toBe(0);
});
}
}
/**
* Check the presence of Error Prompt String on Brackets Window
* @param {String} title - Title String Which will be matched with Update Bar heading.
* @param {String} description - description String Which will be matched with Update Bar description.
*/
function checkPopUpString(title, titleDescription) {
var doc = $(testWindow.document),
errorPopUp = doc.find(".error-dialog.instance"),
heading = errorPopUp.find('.dialog-title'),
description = errorPopUp.find('.dialog-message');
// Test if the update bar has been displayed.
//expect(errorPopUp.length).toBe(1);
if (title) {
expect(heading.text()).toBe(title);
}
if (titleDescription) {
expect(description.text()).toBe(titleDescription);
}
}
function toggleDiagnosisResults(visible) {
var doc = $(testWindow.document),
problemsPanel = doc.find("#problems-panel"),
statusInspection = $("#status-inspection");
statusInspection.triggerHandler("click");
expect(problemsPanel.is(":visible")).toBe(visible);
}
/**
* 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(jumpPromise, callback) {
var cursor = null,
complete = false;
jumpPromise.done(function () {
complete = true;
});
waitsFor(function () {
var activeEditor = EditorManager.getActiveEditor();
cursor = activeEditor.getCursorPos();
return complete;
}, "Expected jump did not occur", 3000);
runs(function () { callback(cursor); });
}
/*
* Expect a given list of hints to be present in a given hint
* response object
*
* @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
*/
function expecthintsPresent(expectedHints) {
var hintObj = (new CodeHintsProvider.CodeHintsProvider(phpToolingExtension.getClient())).getHints(null);
_waitForHints(hintObj, function (hintList) {
expect(hintList).toBeTruthy();
expectedHints.forEach(function (expectedHint) {
expect(_indexOf(hintList, expectedHint)).not.toBe(-1);
});
});
}
/*
* Return the index at which hint occurs in hintList
*
* @param {Array.<Object>} hintList - the list of hints
* @param {string} hint - the hint to search for
* @return {number} - the index into hintList at which the hint occurs,
* or -1 if it does not
*/
function _indexOf(hintList, hint) {
var index = -1,
counter = 0;
for (counter; counter < hintList.length; counter++) {
if (hintList[counter].data("token").label === hint) {
index = counter;
break;
}
}
return index;
}
/*
* 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); });
}
/**
* 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 requestStatus = null;
var request,
complete = false;
runs(function () {
request = (new DefaultProviders.ParameterHintsProvider(phpToolingExtension.getClient()))
.getParameterHints();
request.done(function (status) {
complete = true;
requestStatus = status;
}).fail(function(){
complete = true;
});
});
waitsFor(function () {
return complete;
}, "Expected Parameter hints did not resolve", 3000);
if (expectedParams === null) {
expect(requestStatus).toBe(null);
return;
}
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].label).toBe(expectedParams[i]);
}
}
runs(function() {
expectHint(requestStatus);
});
}
/**
* Show the document/project symbols for a language type.
*
* @param {SymbolProvider} provider The symbol provider to use for the request.
* @param {string} query The query string for the request.
* @param {Array} expectedSymbols Expected results for the request.
*/
function expectSymbols(provider, query, expectedSymbols) {
var requestStatus = null;
var request,
matcher;
runs(function () {
matcher = new StringMatch.StringMatcher();
request = new provider(phpToolingExtension.getClient()).search(query, matcher);
request.done(function (status) {
requestStatus = status;
});
waitsForDone(request, "Expected Symbols did not resolve", 3000);
});
if (expectedSymbols === []) {
expect(requestStatus).toBe([]);
return;
}
function matchSymbols(symbols) {
var n = symbols.length > 4 ? 4 : symbols.length,
i;
for (i = 0; i < n; i++) {
var symbolInfo = symbols[i].symbolInfo;
expect(symbolInfo.label).toBe(expectedSymbols[i].label);
expect(symbolInfo.type).toBe(expectedSymbols[i].type);
expect(symbolInfo.scope).toBe(expectedSymbols[i].scope);
if (expectedSymbols[i].fullPath === null) {
expect(symbolInfo.fullPath).toBe(null);
} else {
expect(symbolInfo.fullPath.includes(expectedSymbols[i].fullPath)).toBe(true);
}
}
}
runs(function() {
matchSymbols(requestStatus);
});
}
/**
* Trigger a jump to definition, and verify that the editor jumped to
* the expected location. The new location is the variable definition
* or function definition of the variable or function at the current
* cursor location. Jumping to the new location will cause a new editor
* to be opened or open an existing editor.
*
* @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(expectedLocation) {
var jumpPromise = (new DefaultProviders.JumpToDefProvider(phpToolingExtension.getClient())).doJumpToDef();
_waitForJump(jumpPromise, 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 expectReferences(referencesExpected) {
var refPromise,
results = null,
complete = false;
runs(function () {
refPromise = (new DefaultProviders.ReferencesProvider(phpToolingExtension.getClient())).getReferences();
refPromise.done(function (resp) {
complete = true;
results = resp;
}).fail(function(){
complete = true;
});
});
waitsFor(function () {
return complete;
}, "Expected Reference Promise did not resolve", 3000);
if(referencesExpected === null) {
expect(results).toBeNull();
return;
}
runs(function() {
expect(results.numFiles).toBe(referencesExpected.numFiles);
expect(results.numMatches).toBe(referencesExpected.numMatches);
expect(results.allResultsAvailable).toBe(referencesExpected.allResultsAvailable);
expect(results.results).not.toBeNull();
for(var key in results.keys) {
expect(results.results.key).toBe(referencesExpected.results.key);
}
});
}
/**
* Check the presence of Error Prompt on Brackets Window
*/
function checkErrorPopUp() {
var doc = $(testWindow.document),
errorPopUp = doc.find(".error-dialog.instance"),
errorPopUpHeader = errorPopUp.find(".modal-header"),
errorPopUpBody = errorPopUp.find(".modal-body"),
errorPopUpFooter = errorPopUp.find(".modal-footer"),
errorPopUpPresent = false;
runs(function () {
expect(errorPopUp.length).toBe(1);
expect(errorPopUpHeader).not.toBeNull();
expect(errorPopUpBody).not.toBeNull();
expect(errorPopUpFooter).not.toBeNull();
});
if (errorPopUp && errorPopUp.length > 0) {
errorPopUpPresent = true;
}
return errorPopUpPresent;
}
it("phpTooling Exiension should be loaded Successfully", function () {
waitForMilliSeconds(5000);
runs(function () {
expect(phpToolingExtension).not.toBeNull();
});
});
it("should attempt to start php server and fail due to lower version of php", function () {
var phpExecutable = testWindow.brackets.platform === "mac" ? "/mac/invalidphp" : "/win/invalidphp";
PreferencesManager.set("php", {
"executablePath": testFolder + phpExecutable
}, {
locations: {scope: "session"}
});
waitForMilliSeconds(5000);
runs(function () {
checkErrorPopUp();
checkPopUpString(Strings.PHP_SERVER_ERROR_TITLE,
StringUtils.format(Strings.PHP_UNSUPPORTED_VERSION, "5.6.30"));
checkPopUpButton("CANCEL");
});
});
it("should attempt to start php server and fail due to invaild executable", function () {
PreferencesManager.set("php", {"executablePath": "/invalidPath/php"}, {locations: {scope: "session"}});
waitForMilliSeconds(5000);
runs(function () {
checkErrorPopUp();
checkPopUpString(Strings.PHP_SERVER_ERROR_TITLE, Strings.PHP_EXECUTABLE_NOT_FOUND);
checkPopUpButton("CANCEL");
});
});
it("should attempt to start php server and fail due to invaild memory limit in prefs settings", function () {
PreferencesManager.set("php", {"memoryLimit": "invalidValue"}, {locations: {scope: "session"}});
waitForMilliSeconds(5000);
runs(function () {
checkErrorPopUp();
checkPopUpString(Strings.PHP_SERVER_ERROR_TITLE, Strings.PHP_SERVER_MEMORY_LIMIT_INVALID);
checkPopUpButton("CANCEL");
});
runs(function () {
SpecRunnerUtils.loadProjectInTestWindow(testFolder + "test");
});
});
it("should attempt to start php server and success", function () {
PreferencesManager.set("php", {"memoryLimit": "4095M"}, {locations: {scope: "session"}});
waitsForDone(SpecRunnerUtils.openProjectFiles([testFile1]), "open test file: " + testFile1);
waitForMilliSeconds(5000);
runs(function () {
toggleDiagnosisResults(false);
toggleDiagnosisResults(true);
});
});
it("should filter hints by query", function () {
waitsForDone(SpecRunnerUtils.openProjectFiles([testFile2]), "open test file: " + testFile2);
runs(function() {
testEditor = EditorManager.getActiveEditor();
testEditor.setCursorPos({ line: 15, ch: 3 });
expecthintsPresent(["$A11", "$A12", "$A13"]);
});
});
it("should show inbuilt functions in hints", function () {
runs(function() {
testEditor = EditorManager.getActiveEditor();
testEditor.setCursorPos({ line: 17, ch: 2 });
expecthintsPresent(["fopen", "for", "foreach"]);
});
});
it("should show static global variables in hints", function () {
runs(function() {
testEditor = EditorManager.getActiveEditor();
testEditor.setCursorPos({ line: 20, ch: 1 });
expecthintsPresent(["$_COOKIE", "$_ENV"]);
});
});
it("should not show parameter hints", function () {
runs(function() {
testEditor = EditorManager.getActiveEditor();
testEditor.setCursorPos({ line: 25, ch: 5 });
expectParameterHint(null);
});
});
it("should show no parameter as a hint", function () {
runs(function() {
testEditor = EditorManager.getActiveEditor();
testEditor.setCursorPos({ line: 27, ch: 19 });
expectParameterHint([], 0);
});
});
it("should show parameters hints", function () {
runs(function() {
testEditor = EditorManager.getActiveEditor();
testEditor.setCursorPos({ line: 26, ch: 9 });
expectParameterHint([
"string $filename",
"string $mode",
"bool $use_include_path = null",
"resource $context = null"], 1);
});
});
it("should not show any references", function () {
var start = { line: 6, ch: 4 };
runs(function () {
testEditor = EditorManager.getActiveEditor();
testEditor.setCursorPos(start);
expectReferences(null);
});
});
it("should show reference present in single file", function () {
var start = { line: 22, ch: 18 },
results = {};
runs(function () {
testEditor = EditorManager.getActiveEditor();
testEditor.setCursorPos(start);
results[testFolder + "test/test2.php"] = {matches: [
{
start: {line: 27, ch: 0},
end: {line: 27, ch: 18},
line: "watchparameterhint()"
}
]
};
expectReferences({
numFiles: 1,
numMatches: 1,
allResultsAvailable: true,
queryInfo: "watchparameterhint",
keys: [testFolder + "test/test2.php"],
results: results
});
});
});
it("should show references present in single file", function () {
var start = { line: 34, ch: 8 },
results = {};
runs(function () {
testEditor = EditorManager.getActiveEditor();
testEditor.setCursorPos(start);
results[testFolder + "test/test2.php"] = {matches: [
{
start: {line: 34, ch: 0},
end: {line: 34, ch: 17},
line: "watchReferences();"
},
{
start: {line: 36, ch: 0},
end: {line: 36, ch: 17},
line: "watchReferences();"
}
]
};
expectReferences({
numFiles: 1,
numMatches: 2,
allResultsAvailable: true,
queryInfo: "watchparameterhint",
keys: [testFolder + "test/test2.php"],
results: results
});
});
});
it("should show references present in multiple files", function () {
var start = { line: 39, ch: 21 },
results = {};
runs(function () {
testEditor = EditorManager.getActiveEditor();
testEditor.setCursorPos(start);
results[testFolder + "test/test2.php"] = {matches: [
{
start: {line: 34, ch: 0},
end: {line: 34, ch: 26},
line: "watchReferences();"
},
{
start: {line: 36, ch: 0},
end: {line: 36, ch: 26},
line: "watchReferences();"
}
]
};
results[testFolder + "test/test3.php"] = {matches: [
{
start: {line: 11, ch: 0},
end: {line: 11, ch: 26},
line: "watchReferences();"
}
]
};
expectReferences({
numFiles: 2,
numMatches: 3,
allResultsAvailable: true,
queryInfo: "watchparameterhint",
keys: [testFolder + "test/test2.php", testFolder + "test/test3.php"],
results: results
});
});
});
it("should jump to earlier defined variable", function () {
var start = { line: 4, ch: 2 };
runs(function () {
testEditor = EditorManager.getActiveEditor();
testEditor.setCursorPos(start);
editorJumped({line: 2, ch: 0});
});
});
it("should jump to class declared in other module file", function () {
var start = { line: 9, ch: 11 };
runs(function () {
testEditor = EditorManager.getActiveEditor();
testEditor.setCursorPos(start);
editorJumped({line: 4, ch: 0, file: "test3.php"});
});
});
it("should fetch document symbols for a given file", function () {
waitsForDone(SpecRunnerUtils.openProjectFiles([testFile4]), "open test file: " + testFile4);
runs(function () {
var provider = SymbolProviders.DocumentSymbolsProvider,
query = "@",
expectedSymbols = [
{
"label": "constantValue",
"fullPath": null,
"type": "Constant",
"scope": "MyClass"
},
{
"label": "MyClass",
"fullPath": null,
"type": "Class",
"scope": ""
},
{
"label": "publicFunction",
"fullPath": null,
"type": "Method",
"scope": "MyClass"
},
{
"label": "publicValue",
"fullPath": null,
"type": "Property",
"scope": "MyClass"
}
];
expectSymbols(provider, query, expectedSymbols);
});
});
it("should fetch no document symbols for a given file", function () {
waitsForDone(SpecRunnerUtils.openProjectFiles([testFile1]), "open test file: " + testFile1);
runs(function () {
var provider = SymbolProviders.DocumentSymbolsProvider,
query = "@",
expectedSymbols = [];
expectSymbols(provider, query, expectedSymbols);
});
});
it("should fetch project symbols for a given file", function () {
runs(function () {
var provider = SymbolProviders.ProjectSymbolsProvider,
query = "#as",
expectedSymbols = [
{
"label": "MyClass",
"fullPath": "test4.php",
"type": "Class",
"scope": ""
},
{
"label": "TestCase",
"fullPath": "test2.php",
"type": "Class",
"scope": "test"
}
];
expectSymbols(provider, query, expectedSymbols);
});
});
});
});