UNPKG

@phoenix-plugin-registry/brackets-xunit

Version:

Open a qUnit, YUI or Jasmine file to run tests

617 lines (554 loc) 25.3 kB
/* * Copyright (c) 2013 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. * * brackets-xunit - a brackets extension to run various unit test frameworks */ /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */ /*global brackets, define, $, window, Mustache */ define(function (require, exports, module) { 'use strict'; var AppInit = brackets.getModule("utils/AppInit"), CommandManager = brackets.getModule("command/CommandManager"), Dialogs = brackets.getModule("widgets/Dialogs"), ExtensionUtils = brackets.getModule("utils/ExtensionUtils"), FileUtils = brackets.getModule("file/FileUtils"), Menus = brackets.getModule("command/Menus"), FileSystem = brackets.getModule("filesystem/FileSystem"), LanguageManager = brackets.getModule("language/LanguageManager"), NodeConnection = brackets.getModule("utils/NodeConnection"), DocumentManager = brackets.getModule("document/DocumentManager"), ProjectManager = brackets.getModule("project/ProjectManager"), qunitRunner = require("main_qunit"), jasmineRunner = require("main_jasmine"), jasmineNodeRunner = require("main_jasmine_node"), nodeRunner = require("main_node"), yuiRunner = require("main_yui"), MyStatusBar = require("MyStatusBar"); var moduledir = FileUtils.getNativeModuleDirectoryPath(module), commands = [], YUITEST_CMD = "yuitest_cmd", JASMINETEST_CMD = "jasminetest_cmd", QUNITTEST_CMD = "qunit_cmd", SCRIPT_CMD = "script_cmd", NODETEST_CMD = "nodetest_cmd", GENERATE_JASMINE_CMD = "generate_jasmine_cmd", GENERATE_QUNIT_CMD = "generate_qunit_cmd", GENERATE_YUI_CMD = "generate_yui_cmd", VIEWHTML_CMD = "viewhtml_cmd", projectMenu = Menus.getContextMenu(Menus.ContextMenuIds.PROJECT_MENU), workingsetMenu = Menus.getContextMenu(Menus.ContextMenuIds.WORKING_SET_MENU), nodeConnection = new NodeConnection(), _windows = {}, enableHtml = false; /* display a modal dialog * title: string * message: string */ function showError(title, message) { Dialogs.showModalDialog( Dialogs.DIALOG_ID_ERROR, title, message ); } // Execute YUI test function runYUI() { yuiRunner.run(); } // Execute Jasmine test function runJasmine() { jasmineRunner.run(); } // Run jasmine-node test, call to node server // when finishes the jasmine.update event is called function runJasmineNode() { jasmineNodeRunner.run(); } // Runs a QUnit test function runQUnit() { qunitRunner.run(); } // opens an html file in a new window function viewHtml() { var entry = ProjectManager.getSelectedItem(); if (entry === undefined) { entry = DocumentManager.getCurrentDocument().file; } var path = entry.fullPath; var w = window.open(path); w.focus(); } // Run a current file as a shell script using node process spawn // results are returned as the script runs from process.stdout, process.stderr // when the script finishes the exit code is returned from the event process.exit function runScript() { nodeRunner.run(); } // parses the current javascript document // an array containing function name and parameters are returned // returns: array of objects { name, params } // params is an array of strings containing parameter names function parseCurrentDocument() { var text = DocumentManager.getCurrentDocument().getText(); var acorn = require('thirdparty/acorn/acorn_loose'); var walk = require('thirdparty/acorn/walk'); var ast = acorn.parse_dammit(text); var i, functions = [], fparams; walk.simple(ast, { FunctionDeclaration: function (node) { fparams = []; for (i = 0; i < node.params.length; i++) { fparams.push(node.params[i].name); } functions.push({name: node.id.name, params: fparams}); } }); return functions; } // createNewFile: for generated tests created a new file in brackets // the file is added to the project (left panel) function createNewFile(fullpath, contents, testExt) { function _getUntitledFileSuggestion(dirPath, baseFileName, fileExt, isFolder) { var result = new $.Deferred(); result.progress(function attemptNewName(suggestedName, nextIndexToUse) { if (nextIndexToUse > 99) { //we've tried this enough result.reject(); return; } //check this name var callback = function (error) { if (error) { //most likely error is FNF, user is better equiped to handle the rest result.resolve(suggestedName); } else { //file exists, notify to the next progress result.notify(baseFileName + "-" + nextIndexToUse + fileExt, nextIndexToUse + 1); } }; if (isFolder) { FileSystem.resolve( basedir + suggestedName, callback ); } else { FileSystem.resolve( basedir + suggestedName, callback ); } }); //kick it off result.notify(baseFileName + fileExt, 1); return result.promise(); } var basedir = fullpath.substring(0, fullpath.lastIndexOf("/")), name = fullpath.substring(fullpath.lastIndexOf("/") + 1), testname = name.substring(0, name.lastIndexOf('.js')) + testExt; var deferred = _getUntitledFileSuggestion(basedir, testname, ".js", false); var createWithSuggestedName = function (suggestedName) { var result = ProjectManager.createNewItem(basedir, suggestedName, true, false); result.done(function (entry) { DocumentManager.getDocumentForPath(entry.fullPath).done(function (doc) { doc.setText(contents); }); }); }; deferred.done(createWithSuggestedName); return deferred; } // returns a global object per test type to pass jslint function generateGlobalDeclaration(type, test, functions) { var fnames = '', i; switch (type) { case "jasmine": test += '/*global describe, it, expect'; break; case "qunit": test += '/*global test, ok, equal'; break; case "yui": test += '/*global YUI'; break; } for (i = 0; i < functions.length; i++) { fnames += ", " + functions[i].name; } test += fnames + " */\n"; return test; } function generateJasmineTest() { var functions = parseCurrentDocument(), fullpath = DocumentManager.getCurrentDocument().file.fullPath, filename = DocumentManager.getCurrentDocument().file.name, contents = DocumentManager.getCurrentDocument().getText(), userequire = contents.match(/define\w*\(/), i, j, fparamstr; if (functions.length === 0) { showError("Generate Jasmine Test", "Warning: The file " + filename + " does not have any methods to test."); return; } var test = '// generated by xUnit ' + new Date() + '\n' + '// jasmine unit test for ' + filename + '\n'; if (userequire) { test = generateGlobalDeclaration("jasmine", test, functions); test += 'define(function (require, exports, module) {\n' + " 'use strict';\n" + " var testapi = require('./" + filename + "');\n"; for (i = 0; i < functions.length; i++) { fparamstr = ''; for (j = 0; j < functions[i].params.length; j++) { if (j > 0) { fparamstr += ", "; } fparamstr += functions[i].params[j]; } test += ' describe' + '("test ' + functions[i].name + '(' + fparamstr + ')", function () {\n'; if (fparamstr !== '') { test += ' var ' + fparamstr + ';\n'; } test += ' it("' + functions[i].name + '(' + fparamstr + ') === ?", function () {\n' + ' expect(testapi.' + functions[i].name + '(' + fparamstr + ')).toEqual("?");\n' + ' });\n' + ' });\n'; } test += '});\n'; } else { test += '// brackets-xunit: includes=' + filename + '\n'; test = generateGlobalDeclaration("jasmine", test, functions); for (i = 0; i < functions.length; i++) { fparamstr = ''; for (j = 0; j < functions[i].params.length; j++) { if (j > 0) { fparamstr += ", "; } fparamstr += functions[i].params[j]; } test += 'describe' + '("test ' + functions[i].name + '(' + fparamstr + ')", function () {\n'; test += ' "use strict";\n'; if (fparamstr !== '') { test += ' var ' + fparamstr + ';\n'; } test += ' it("' + functions[i].name + '(' + fparamstr + ') === ?", function () {\n' + ' expect(' + functions[i].name + '(' + fparamstr + ')).toEqual("?");\n' + ' });\n' + '});\n'; } } createNewFile(fullpath, test, ".spec"); } function generateQunitTest() { var functions = parseCurrentDocument(), filename = DocumentManager.getCurrentDocument().file.name, i, j, fparamstr, fullpath = DocumentManager.getCurrentDocument().file.fullPath; if (functions.length === 0) { showError("Generate Qunit Test", "Warning: The file " + filename + " does not have any methods to test."); return; } var test = '// generated by xUnit ' + new Date() + '\n' + '// qunit test for ' + filename + '\n' + '// brackets-xunit:' + ' qunit\n' + '// brackets-xunit: includes=' + filename + '\n'; test = generateGlobalDeclaration("qunit", test, functions); for (i = 0; i < functions.length; i++) { fparamstr = ''; for (j = 0; j < functions[i].params.length; j++) { if (j > 0) { fparamstr += ", "; } fparamstr += functions[i].params[j]; } test += 'test' + '("test ' + functions[i].name + '(' + fparamstr + ')", function () {\n'; if (fparamstr !== '') { test += ' "use strict";\n'; test += ' var ' + fparamstr + ';\n'; } test += ' equal' + '(' + functions[i].name + '(' + fparamstr + '), "???", "' + functions[i].name + '(' + fparamstr + ') equals ???");\n'; test += '});\n'; } createNewFile(fullpath, test, ".qunit"); } function generateYuiTest() { var functions = parseCurrentDocument(), filename = DocumentManager.getCurrentDocument().file.name, i, j, fparamstr, fullpath = DocumentManager.getCurrentDocument().file.fullPath; if (functions.length === 0) { showError("Generate YUI Test", "Warning: The file " + filename + " does not have any methods to test."); return; } var test = '// generated by xUnit ' + new Date() + '\n' + '// YUI test for ' + filename + '\n' + '// brackets-xunit:' + ' yui\n' + '// brackets-xunit: includes=' + filename + '\n\n'; test = generateGlobalDeclaration("yui", test, functions); test += "YUI({ logInclude: {TestRunner: true }}).use('test', 'test-console', function (Y) {\n" + " 'use strict';\n" + ' var testCase = new Y.Test.Case({\n' + ' name: " test ' + filename + ' functions",\n'; for (i = 0; i < functions.length; i++) { fparamstr = ''; for (j = 0; j < functions[i].params.length; j++) { if (j > 0) { fparamstr += ", "; } fparamstr += functions[i].params[j]; } if (i > 0) { test += ',\n'; } test += ' test' + functions[i].name + ': function () {\n'; if (fparamstr !== '') { test += ' var ' + fparamstr + ';\n'; } test += ' Y.assert(' + functions[i].name + '(' + fparamstr + ') === "?", "test ' + functions[i].name + '(' + fparamstr + ') === ?");\n' + ' }'; } test += '\n });\n' + ' Y.Test.Runner.add(testCase);\n' + ' (new Y.Test.Console({\n' + ' newestOnTop: false\n' + " })).render('#log');\n" + " Y.Test.Runner" + ".run();\n" + "});\n"; createNewFile(fullpath, test, ".yui"); } // converts time in ms to a more readable string format // e.g. 1h 10m 30.2s function formatTime(ms) { var result = "", secs = ms / 1000; if (secs >= 60 * 60 * 24 * 365) { result = (Math.floor(secs / (60 * 60 * 24 * 365))) + "y "; secs = secs % (60 * 60 * 24 * 365); } if (secs >= 60 * 60 * 24) { result = (Math.floor(secs / (60 * 60 * 24))) + "d "; secs = secs % (60 * 60 * 24); } if (secs >= 60 * 60) { result = result + (Math.floor(secs / (60 * 60))) + "h "; secs = secs % (60 * 60); } if (secs >= 60) { result = result + (Math.floor(secs / 60)) + "m "; secs = secs % 60; } if (result === "" || secs > 0) { result = result + Math.round(10 * secs) / 10 + "s"; } if (result[result.length - 1] === " ") { result = result.substring(0, result.length - 1); } return result; } // reads config.js to determine if brackets-xunit should be disabled for the current project function readConfig() { var result = new $.Deferred(); var root = ProjectManager.getProjectRoot(), configFile = FileSystem.getFileForPath(root.fullPath + "config.js"); FileUtils.readAsText(configFile).done(function (text) { try { var config = JSON.parse(text); if (config.hasOwnProperty('brackets-xunit') && config['brackets-xunit'] === 'disable') { result.reject('disabled'); } } catch (e) { console.log("[brackets-xunit] reading " + root.fullPath + "config.js Error " + e); } finally { return result.resolve('ok'); } }).fail(function () { return result.resolve('ok'); }); return result.promise(); } // determine if a file is a known test type // first look for brackets-xunit: [type], takes precedence // next look for distinguishing clues in the file: // YUI: 'YUI(' and 'Test.runner.test' // jasmine: 'describe' and 'it' // QUnit: 'test()' and 'it()' function determineFileType(fileEntry, text) { if (text && text.match(/^#!/) !== null) { return "script"; } else if (text && fileEntry && fileEntry.fullPath && fileEntry.fullPath.match(/\.js$/)) { if (text.match(/brackets-xunit:\s*yui/i) !== null) { return "yui"; } else if (text.match(/define\(/i) && text.match(/describe\s*\(/)) { return "jasmine"; } else if (text.match(/brackets-xunit:\s*jasmine-node/i)) { return "node"; } else if (text.match(/brackets-xunit:\s*jasmine/i) !== null) { return "jasmine"; } else if (text.match(/brackets-xunit:\s*qunit/i) !== null) { return "qunit"; } else if (text.match(/describe\s*\(/) && text.match(/it\s*\(/)) { return "jasmine"; } else if (text.match(/YUI\s*\(/) && text.match(/Test\.Runner\.run\s*\(/)) { return "yui"; } else if (text.match(/test\s*\(/) && text.match(/ok\s*\(/)) { return "qunit"; } else { return "generate"; } } else if (fileEntry && fileEntry.fullPath && fileEntry.fullPath.match(/\.html$/)) { return "html"; } else { return "unknown"; } } /* * cleanMenu - removes all brackets-xunit menu items from a menu * parameters: menu - the WorkingSetMenu or the ProjectMenu */ function cleanMenu(menu) { var i; for (i = 0; i < commands.length; i++) { menu.removeMenuItem(commands[i]); } } /* * checkFileTypes - adds a menuitem to a menu if the current file matches a known type * parameters: menu - the context menu or working files menu * entry - the current file entry object containing the file name * text - the contents of the current file */ function checkFileTypes(menu, entry, text) { if (entry === null) { return "unknown"; } var type = determineFileType(entry, text); if (type === "yui") { menu.addMenuItem(YUITEST_CMD, "", Menus.LAST); } else if (type === "jasmine") { menu.addMenuItem(JASMINETEST_CMD, "", Menus.LAST); } else if (type === "qunit") { menu.addMenuItem(QUNITTEST_CMD, "", Menus.LAST); } else if (type === "script") { menu.addMenuItem(SCRIPT_CMD, "", Menus.LAST); } else if (type === "node") { menu.addMenuItem(NODETEST_CMD, "", Menus.LAST); } else if (enableHtml && type === "html") { menu.addMenuItem(VIEWHTML_CMD, "", Menus.LAST); } if (type === "generate") { menu.addMenuItem(GENERATE_JASMINE_CMD, "", Menus.LAST); menu.addMenuItem(GENERATE_QUNIT_CMD, "", Menus.LAST); menu.addMenuItem(GENERATE_YUI_CMD, "", Menus.LAST); } } // setup, connects to the node server loads node/JasmineDomain and node/ProcessDomain AppInit.appReady(function () { /*Globals for passing objects from run windows*/ window.reportComplete = function (result) { if (result.status === "failed") { MyStatusBar.statusFailed(result.message); MyStatusBar.toggleCollapsed(false); } else { MyStatusBar.statusPassed(result.message); } }; window.reportUpdate = function (result) { console.log("update", result); MyStatusBar.statusRunning(result.message); }; window.coverageComplete = function (result) { MyStatusBar.statusCoverage(result.message); }; function runTestsOnSaveOrChange(document) { var language = document ? LanguageManager.getLanguageForPath(document.file.fullPath) : ""; if (language && language.getId() === "javascript") { var selectedEntry = document.file, text = document.getText(), type; readConfig().done(function () { type = determineFileType(selectedEntry, text); if (type === "yui") { $("#status-language").text("Javascript+YUI"); runYUI(); } else if (type === "jasmine") { $("#status-language").text("Javascript+Jasmine"); runJasmine(); } else if (type === "qunit") { $("#status-language").text("Javascript+QUnit"); runQUnit(); } else if (type === "node") { $("#status-language").text("Node+Jasmine"); runJasmineNode(); } }); } } $(DocumentManager) .on("documentSaved.xunit", function (e, d) { runTestsOnSaveOrChange(d); }); $(DocumentManager) .on("currentDocumentChange", function () { runTestsOnSaveOrChange(DocumentManager.getCurrentDocument()); }); MyStatusBar.initializePanel(); }); // Register commands as right click menu items commands = [ YUITEST_CMD, JASMINETEST_CMD, QUNITTEST_CMD, SCRIPT_CMD, NODETEST_CMD, GENERATE_JASMINE_CMD, GENERATE_QUNIT_CMD, GENERATE_YUI_CMD, VIEWHTML_CMD]; CommandManager.register("Run YUI Unit Test", YUITEST_CMD, runYUI); CommandManager.register("Run Jasmine xUnit Test", JASMINETEST_CMD, runJasmine); CommandManager.register("Run QUnit xUnit Test", QUNITTEST_CMD, runQUnit); CommandManager.register("Run Script", SCRIPT_CMD, runScript); CommandManager.register("Run Jasmine-Node xUnit Test", NODETEST_CMD, runJasmineNode); CommandManager.register("Generate Jasmine xUnit Test", GENERATE_JASMINE_CMD, generateJasmineTest); CommandManager.register("Generate Qunit xUnit Test", GENERATE_QUNIT_CMD, generateQunitTest); CommandManager.register("Generate YUI xUnit Test", GENERATE_YUI_CMD, generateYuiTest); CommandManager.register("xUnit View html", VIEWHTML_CMD, viewHtml); // check if the extension should add a menu item to the project menu (under the project name, left panel) $(projectMenu).on("beforeContextMenuOpen", function () { var selectedEntry = ProjectManager.getSelectedItem(), text = ''; if (selectedEntry && selectedEntry.fullPath && DocumentManager.getCurrentDocument() !== null && selectedEntry.fullPath === DocumentManager.getCurrentDocument().file.fullPath) { text = DocumentManager.getCurrentDocument().getText(); } cleanMenu(projectMenu); readConfig().done(function () { checkFileTypes(projectMenu, selectedEntry, text); }); }); // check if the extension should add a menu item to the workingset menu (under Working Files, left panel) $(workingsetMenu).on("beforeContextMenuOpen", function () { var selectedEntry = DocumentManager.getCurrentDocument().file, text = DocumentManager.getCurrentDocument().getText(); cleanMenu(workingsetMenu); readConfig().done(function () { checkFileTypes(workingsetMenu, selectedEntry, text); }); }); exports.formatTime = formatTime; exports.checkFileTypes = checkFileTypes; exports.determineFileType = determineFileType; });