/
githubmirror
/
brackets
Обзор
Документация
Войти
/
githubmirror
/
brackets
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
test/spec/SpecRunnerUtils.js
1 434 строки
54 KB
Pete Nykänen
Issue #11261: keep the search bar open (#14141)
02 мар 2018, 17:56
Не верифицирован
02 мар 2018, 17:56
32300dc
Код
Авторство
О чём код?
/* * 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 jasmine, expect, beforeEach, waitsFor, waitsForDone, runs, spyOn, KeyboardEvent */ define(function (require, exports, module) { 'use strict'; var Commands = require("command/Commands"), FileUtils = require("file/FileUtils"), Async = require("utils/Async"), DocumentManager = require("document/DocumentManager"), Editor = require("editor/Editor").Editor, EditorManager = require("editor/EditorManager"), MainViewManager = require("view/MainViewManager"), FileSystemError = require("filesystem/FileSystemError"), FileSystem = require("filesystem/FileSystem"), WorkspaceManager = require("view/WorkspaceManager"), ExtensionLoader = require("utils/ExtensionLoader"), UrlParams = require("utils/UrlParams").UrlParams, LanguageManager = require("language/LanguageManager"); var TEST_PREFERENCES_KEY = "com.adobe.brackets.test.preferences", EDITOR_USE_TABS = false, EDITOR_SPACE_UNITS = 4, OPEN_TAG = "{{", RE_MARKER = /\{\{(\d+)\}\}/g, absPathPrefix = (brackets.platform === "win" ? "c:/" : "/"), _testSuites = {}, _testWindow, _doLoadExtensions, _rootSuite = { id: "__brackets__" }, _unitTestReporter; MainViewManager._initialize($("#mock-main-view")); function _getFileSystem() { return _testWindow ? _testWindow.brackets.test.FileSystem : FileSystem; } /** * Delete a path * @param {string} fullPath * @param {boolean=} silent Defaults to false. When true, ignores ERR_NOT_FOUND when deleting path. * @return {$.Promise} Resolved when deletion complete, or rejected if an error occurs */ function deletePath(fullPath, silent) { var result = new $.Deferred(); _getFileSystem().resolve(fullPath, function (err, item) { if (!err) { item.unlink(function (err) { if (!err) { result.resolve(); } else { if (err === FileSystemError.NOT_FOUND && silent) { result.resolve(); } else { console.error("Unable to remove " + fullPath, err); result.reject(err); } } }); } else { if (err === FileSystemError.NOT_FOUND && silent) { result.resolve(); } else { console.error("Unable to remove " + fullPath, err); result.reject(err); } } }); return result.promise(); } /** * Set permissions on a path * @param {!string} path Path to change permissions on * @param {!string} mode New mode as an octal string * @return {$.Promise} Resolved when permissions are set or rejected if an error occurs */ function chmod(path, mode) { var deferred = new $.Deferred(); brackets.fs.chmod(path, parseInt(mode, 8), function (err) { if (err) { deferred.reject(err); } else { deferred.resolve(); } }); return deferred.promise(); } function testDomain() { return brackets.testing.nodeConnection.domains.testing; } /** * Remove a directory (recursively) or file * * @param {!string} path Path to remove * @return {$.Promise} Resolved when the path is removed, rejected if there was a problem */ function remove(path) { return testDomain().remove(path); } /** * Copy a directory (recursively) or file * * @param {!string} src Path to copy * @param {!string} dest Destination directory * @return {$.Promise} Resolved when the path is copied, rejected if there was a problem */ function copy(src, dest) { return testDomain().copy(src, dest); } /** * Rename a directory or file. * @return {$.Promise} Resolved when the path is rename, rejected if there was a problem */ function rename(src, dest) { return testDomain().rename(src, dest); } /** * Resolves a path string to a File or Directory * @param {!string} path Path to a file or directory * @return {$.Promise} A promise resolved when the file/directory is found or * rejected when any error occurs. */ function resolveNativeFileSystemPath(path) { var result = new $.Deferred(); _getFileSystem().resolve(path, function (err, item) { if (!err) { result.resolve(item); } else { result.reject(err); } }); return result.promise(); } /** * Utility for tests that wait on a Promise to complete. Placed in the global namespace so it can be used * similarly to the standard Jasmine waitsFor(). Unlike waitsFor(), must be called from INSIDE * the runs() that generates the promise. * @param {$.Promise} promise * @param {string} operationName Name used for timeout error message */ window.waitsForDone = function (promise, operationName, timeout) { timeout = timeout || 1000; expect(promise).toBeTruthy(); promise.fail(function (err) { expect("[" + operationName + "] promise rejected with: " + err).toBe("(expected resolved instead)"); }); waitsFor(function () { return promise.state() === "resolved"; }, "success [" + operationName + "]", timeout); }; /** * Utility for tests that waits on a Promise to fail. Placed in the global namespace so it can be used * similarly to the standards Jasmine waitsFor(). Unlike waitsFor(), must be called from INSIDE * the runs() that generates the promise. * @param {$.Promise} promise * @param {string} operationName Name used for timeout error message */ window.waitsForFail = function (promise, operationName, timeout) { timeout = timeout || 1000; expect(promise).toBeTruthy(); promise.done(function (result) { expect("[" + operationName + "] promise resolved with: " + result).toBe("(expected rejected instead)"); }); waitsFor(function () { return promise.state() === "rejected"; }, "failure " + operationName, timeout); }; /** * Get or create a NativeFileSystem rooted at the system root. * @return {$.Promise} A promise resolved when the native file system is found or rejected when an error occurs. */ function getRoot() { var deferred = new $.Deferred(); resolveNativeFileSystemPath("/").then(deferred.resolve, deferred.reject); return deferred.promise(); } function getTestRoot() { // /path/to/brackets/test/SpecRunner.html var path = window.location.pathname; path = path.substr(0, path.lastIndexOf("/")); path = FileUtils.convertToNativePath(path); return path; } function getTestPath(path) { return getTestRoot() + path; } /** * Get the temporary unit test project path. Use this path for unit tests that need to modify files on disk. * @return {$.string} Path to the temporary unit test project */ function getTempDirectory() { return getTestPath("/temp"); } /** * Create the temporary unit test project directory. */ function createTempDirectory() { var deferred = new $.Deferred(); runs(function () { _getFileSystem().getDirectoryForPath(getTempDirectory()).create(function (err) { if (err && err !== FileSystemError.ALREADY_EXISTS) { deferred.reject(err); } else { deferred.resolve(); } }); }); waitsForDone(deferred, "Create temp directory", 500); } function _resetPermissionsOnSpecialTempFolders() { var entries = [], result = new $.Deferred(), promise, entryPromise = new $.Deferred(), tempDir; function visitor(entry) { entries.push(entry.fullPath); return true; } tempDir = FileSystem.getDirectoryForPath(getTempDirectory()); tempDir.visit(visitor, function(err){ if (!err) { entryPromise.resolve(entries); } else { if (err === FileSystemError.NOT_FOUND) { entryPromise.resolve(entries); } else { entryPromise.reject(); } } }); entryPromise.done(function(entries){ promise = Async.doSequentially(entries, function (entry) { var deferred = new $.Deferred(); FileSystem.resolve(entry, function (err, item) { if (!err) { // Change permissions if the directory exists chmod(entry, "777").then(deferred.resolve, deferred.reject); } else { if (err === FileSystemError.NOT_FOUND) { // Resolve the promise since the folder to reset doesn't exist deferred.resolve(); } else { deferred.reject(); } } }); return deferred.promise(); }, true); promise.then(result.resolve, result.reject); }).fail(function() { result.reject(); }); return result.promise(); } /** * Remove temp folder used for temporary unit tests files */ function removeTempDirectory() { var deferred = new $.Deferred(), baseDir = getTempDirectory(); runs(function () { _resetPermissionsOnSpecialTempFolders().done(function () { deletePath(baseDir, true).then(deferred.resolve, deferred.reject); }).fail(function () { deferred.reject(); }); deferred.fail(function (err) { console.log("boo"); }); waitsForDone(deferred.promise(), "removeTempDirectory", 2000); }); } function getBracketsSourceRoot() { var path = window.location.pathname; path = path.split("/"); path = path.slice(0, path.length - 2); path.push("src"); return path.join("/"); } /** * Returns a Document suitable for use with an Editor in isolation, but that can be registered with * DocumentManager via addRef() so it is maintained for global updates like name and language changes. * * Like a normal Document, if you cause an addRef() on this you MUST call releaseRef() later. * * @param {!{language:?string, filename:?string, content:?string }} options * Language defaults to JavaScript, filename defaults to a placeholder name, and * content defaults to "". */ function createMockActiveDocument(options) { var language = options.language || LanguageManager.getLanguage("javascript"), filename = options.filename || (absPathPrefix + "_unitTestDummyPath_/_dummyFile_" + Date.now() + "." + language._fileExtensions[0]), content = options.content || ""; // Use unique filename to avoid collissions in open documents list var dummyFile = _getFileSystem().getFileForPath(filename); var docToShim = new DocumentManager.Document(dummyFile, new Date(), content); // Prevent adding doc to working set by not dispatching "dirtyFlagChange". // TODO: Other functionality here needs to be kept in sync with Document._handleEditorChange(). In the // future, we should fix things so that we either don't need mock documents or that this // is factored so it will just run in both. docToShim._handleEditorChange = function (event, editor, changeList) { this.isDirty = !editor._codeMirror.isClean(); this._notifyDocumentChange(changeList); }; docToShim.notifySaved = function () { throw new Error("Cannot notifySaved() a unit-test dummy Document"); }; return docToShim; } /** * Returns a Document suitable for use with an Editor in isolation: i.e., a Document that will * never be set as the currentDocument or added to the working set. * * Unlike a real Document, does NOT need to be explicitly cleaned up. * * @param {?string} initialContent Defaults to "" * @param {?string} languageId Defaults to JavaScript * @param {?string} filename Defaults to an auto-generated filename with the language's extension */ function createMockDocument(initialContent, languageId, filename) { var language = LanguageManager.getLanguage(languageId) || LanguageManager.getLanguage("javascript"), options = { language: language, content: initialContent, filename: filename }, docToShim = createMockActiveDocument(options); // Prevent adding doc to global 'open docs' list; prevents leaks or collisions if a test // fails to clean up properly (if test fails, or due to an apparent bug with afterEach()) docToShim.addRef = function () {}; docToShim.releaseRef = function () {}; docToShim._ensureMasterEditor = function () { if (!this._masterEditor) { // Don't let Document create an Editor itself via EditorManager; the unit test can't clean that up throw new Error("Use create/destroyMockEditor() to test edit operations"); } }; return docToShim; } /** * Returns a mock element (in the test runner window) that's offscreen, for * parenting UI you want to unit-test. When done, make sure to delete it with * remove(). * @return {jQueryObject} a jQuery object for an offscreen div */ function createMockElement() { return $("<div/>") .css({ position: "absolute", left: "-10000px", top: "-10000px" }) .appendTo($("body")); } function createEditorInstance(doc, pane, visibleRange) { var $editorHolder = pane.$el || pane; // To handle actual pane mock or a fake container var editor = new Editor(doc, true, $editorHolder.get(0), visibleRange); Editor.setUseTabChar(EDITOR_USE_TABS); Editor.setSpaceUnits(EDITOR_SPACE_UNITS); if (pane.addView) { pane.addView(editor); editor._paneId = pane.id; } EditorManager._notifyActiveEditorChanged(editor); return editor; } /** * Returns an Editor tied to the given Document, but suitable for use in isolation * (without being placed inside the surrounding Brackets UI). The Editor *will* be * reported as the "active editor" by EditorManager. * * Must be cleaned up by calling destroyMockEditor(document) later. * * @param {!Document} doc * @param {{startLine: number, endLine: number}=} visibleRange * @return {!Editor} */ function createMockEditorForDocument(doc, visibleRange) { // Initialize EditorManager/WorkspaceManager and position the editor-holder offscreen // (".content" may not exist, but that's ok for headless tests where editor height doesn't matter) var $editorHolder = createMockElement().css("width", "1000px").attr("id", "hidden-editors"); WorkspaceManager._setMockDOM($(".content"), $editorHolder); // create Editor instance return createEditorInstance(doc, $editorHolder, visibleRange); } /** * Returns a Document and Editor suitable for use in isolation: the Document * will never be set as the currentDocument or added to the working set and the * Editor does not live inside a full-blown Brackets UI layout. The Editor *will* be * reported as the "active editor" by EditorManager, however. * * Must be cleaned up by calling destroyMockEditor(document) later. * * @param {string=} initialContent * @param {string=} languageId * @param {{startLine: number, endLine: number}=} visibleRange * @return {!{doc:!Document, editor:!Editor}} */ function createMockEditor(initialContent, languageId, visibleRange) { // create dummy Document, then Editor tied to it var doc = createMockDocument(initialContent, languageId); return { doc: doc, editor: createMockEditorForDocument(doc, visibleRange) }; } function createMockPane($el, paneId) { createMockElement() .attr("class", "pane-header") .appendTo($el); var $fakeContent = createMockElement() .attr("class", "pane-content") .appendTo($el); return { $el: $el, id: paneId || 'first-pane', $content: $fakeContent, addView: function (editor) { this.$content.append(editor.$el); }, showView: function (editor) { } }; } /** * Destroy the Editor instance for a given mock Document. * @param {!Document} doc Document whose master editor to destroy */ function destroyMockEditor(doc) { EditorManager._notifyActiveEditorChanged(null); MainViewManager._destroyEditorIfNotNeeded(doc); // Clear editor holder so EditorManager doesn't try to resize destroyed object $("#hidden-editors").remove(); } /** * Dismiss the currently open dialog as if the user had chosen the given button. Dialogs close * asynchronously; after calling this, you need to start a new runs() block before testing the * outcome. Also, in cases where asynchronous tasks are performed after the dialog closes, * clients must also wait for any additional promises. * @param {string} buttonId One of the Dialogs.DIALOG_BTN_* symbolic constants. * @param {boolean=} enableFirst If true, then enable the button before clicking. */ function clickDialogButton(buttonId, enableFirst) { // Make sure there's one and only one dialog open var $dlg = _testWindow.$(".modal.instance"), promise = $dlg.data("promise"); expect($dlg.length).toBe(1); // Make sure desired button exists var $dismissButton = $dlg.find(".dialog-button[data-button-id='" + buttonId + "']"); expect($dismissButton.length).toBe(1); if (enableFirst) { // Remove the disabled prop. $dismissButton.prop("disabled", false); } // Click the button $dismissButton.click(); // Dialog should resolve/reject the promise waitsForDone(promise, "dismiss dialog"); } function createTestWindowAndRun(spec, callback, options) { runs(function () { // Position popup windows in the lower right so they're out of the way var testWindowWid = 1000, testWindowHt = 700, testWindowX = window.screen.availWidth - testWindowWid, testWindowY = window.screen.availHeight - testWindowHt, optionsStr = "left=" + testWindowX + ",top=" + testWindowY + ",width=" + testWindowWid + ",height=" + testWindowHt; var params = new UrlParams(); // setup extension loading in the test window params.put("extensions", _doLoadExtensions ? "default,dev," + ExtensionLoader.getUserExtensionPath() : "default"); // disable update check in test windows params.put("skipUpdateCheck", true); // disable loading of sample project params.put("skipSampleProjectLoad", true); // disable initial dialog for live development params.put("skipLiveDevelopmentInfo", true); // signals that main.js should configure RequireJS for tests params.put("testEnvironment", true); if (options) { // option to set the params if (options.hasOwnProperty("params")) { var paramObject = options.params || {}; var obj; for (obj in paramObject) { if (paramObject.hasOwnProperty(obj)) { params.put(obj, paramObject[obj]); } } } // option to launch test window with either native or HTML menus if (options.hasOwnProperty("hasNativeMenus")) { params.put("hasNativeMenus", (options.hasNativeMenus ? "true" : "false")); } } _testWindow = window.open(getBracketsSourceRoot() + "/index.html?" + params.toString(), "_blank", optionsStr); // Displays the primary console messages from the test window in the // test runner's console as well. ["debug", "log", "info", "warn", "error"].forEach(function (method) { var originalMethod = _testWindow.console[method]; _testWindow.console[method] = function () { var log = ["[testWindow] "].concat(Array.prototype.slice.call(arguments, 0)); console[method].apply(console, log); originalMethod.apply(_testWindow.console, arguments); }; }); _testWindow.isBracketsTestWindow = true; _testWindow.executeCommand = function executeCommand(cmd, args) { return _testWindow.brackets.test.CommandManager.execute(cmd, args); }; _testWindow.closeAllFiles = function closeAllFiles() { runs(function () { var promise = _testWindow.executeCommand(_testWindow.brackets.test.Commands.FILE_CLOSE_ALL); _testWindow.brackets.test.Dialogs.cancelModalDialogIfOpen( _testWindow.brackets.test.DefaultDialogs.DIALOG_ID_SAVE_CLOSE, _testWindow.brackets.test.DefaultDialogs.DIALOG_BTN_DONTSAVE ); waitsForDone(promise, "Close all open files in working set"); }); }; }); // FIXME (issue #249): Need an event or something a little more reliable... waitsFor( function isBracketsDoneLoading() { return _testWindow.brackets && _testWindow.brackets.test && _testWindow.brackets.test.doneLoading; }, "brackets.test.doneLoading", 10000 ); runs(function () { // callback allows specs to query the testWindow before they run callback.call(spec, _testWindow); }); } function closeTestWindow() { // debug-only to see testWindow state before closing // waits(500); runs(function () { //we need to mark the documents as not dirty before we close //or the window will stay open prompting to save var openDocs = _testWindow.brackets.test.DocumentManager.getAllOpenDocuments(); openDocs.forEach(function resetDoc(doc) { if (doc.isDirty) { //just refresh it back to it's current text. This will mark it //clean to save doc.refreshText(doc.getText(), doc.diskTimestamp); } }); _testWindow.close(); _testWindow.executeCommand = null; _testWindow = null; }); } function loadProjectInTestWindow(path) { runs(function () { // begin loading project path var result = _testWindow.brackets.test.ProjectManager.openProject(path); // wait for file system to finish loading waitsForDone(result, "ProjectManager.openProject()", 10000); }); } /** * Parses offsets from text offset markup (e.g. "{{1}}" for offset 1). * @param {!string} text Text to parse * @return {!{offsets:!Array.<{line:number, ch:number}>, text:!string, original:!string}} */ function parseOffsetsFromText(text) { var offsets = [], output = [], i = 0, line = 0, charAt = 0, ch = 0, length = text.length, exec = null, found = false; while (i < length) { found = false; if (text.slice(i, i + OPEN_TAG.length) === OPEN_TAG) { // find "{{[0-9]+}}" RE_MARKER.lastIndex = i; exec = RE_MARKER.exec(text); found = (exec !== null && exec.index === i); if (found) { // record offset info offsets[exec[1]] = {line: line, ch: ch}; // advance i += exec[0].length; } } if (!found) { charAt = text.substr(i, 1); output.push(charAt); if (charAt === '\n') { line++; ch = 0; } else { ch++; } i++; } } return {offsets: offsets, text: output.join(""), original: text}; } /** * Creates absolute paths based on the test window's current project * @param {!Array.<string>|string} paths Project relative file path(s) to convert. May pass a single string path or array. * @return {!Array.<string>|string} Absolute file path(s) */ function makeAbsolute(paths) { var fullPath = _testWindow.brackets.test.ProjectManager.getProjectRoot().fullPath; function prefixProjectPath(path) { if (path.indexOf(fullPath) === 0) { return path; } return fullPath + path; } if (Array.isArray(paths)) { return paths.map(prefixProjectPath); } else { return prefixProjectPath(paths); } } /** * Creates relative paths based on the test window's current project. Any paths, * outside the project are included in the result, but left as absolute paths. * @param {!Array.<string>|string} paths Absolute file path(s) to convert. May pass a single string path or array. * @return {!Array.<string>|string} Relative file path(s) */ function makeRelative(paths) { var fullPath = _testWindow.brackets.test.ProjectManager.getProjectRoot().fullPath, fullPathLength = fullPath.length; function removeProjectPath(path) { if (path.indexOf(fullPath) === 0) { return path.substring(fullPathLength); } return path; } if (Array.isArray(paths)) { return paths.map(removeProjectPath); } else { return removeProjectPath(paths); } } function makeArray(arg) { if (!Array.isArray(arg)) { return [arg]; } return arg; } /** * Opens project relative file paths in the test window * @param {!(Array.<string>|string)} paths Project relative file path(s) to open * @return {!$.Promise} A promise resolved with a mapping of project-relative path * keys to a corresponding Document */ function openProjectFiles(paths) { var result = new $.Deferred(), fullpaths = makeArray(makeAbsolute(paths)), keys = makeArray(makeRelative(paths)), docs = {}, FileViewController = _testWindow.brackets.test.FileViewController, DocumentManager = _testWindow.brackets.test.DocumentManager; Async.doSequentially(fullpaths, function (path, i) { var one = new $.Deferred(); FileViewController.openFileAndAddToWorkingSet(path).done(function (file) { docs[keys[i]] = DocumentManager.getOpenDocumentForPath(file.fullPath); one.resolve(); }).fail(function (err) { one.reject(err); }); return one.promise(); }, false).done(function () { result.resolve(docs); }).fail(function (err) { result.reject(err); }).always(function () { docs = null; FileViewController = null; }); return result.promise(); } /** * Create or overwrite a text file * @param {!string} path Path for a file to be created/overwritten * @param {!string} text Text content for the new file * @param {!FileSystem} fileSystem FileSystem instance to use. Normally, use the instance from * testWindow so the test copy of Brackets is aware of the newly-created file. * @return {$.Promise} A promise resolved when the file is written or rejected when an error occurs. */ function createTextFile(path, text, fileSystem) { var deferred = new $.Deferred(), file = fileSystem.getFileForPath(path), options = { blind: true // overwriting previous files is OK }; file.write(text, options, function (err) { if (!err) { deferred.resolve(file); } else { deferred.reject(err); } }); return deferred.promise(); } /** * Copy a file source path to a destination * @param {!File} source Entry for the source file to copy * @param {!string} destination Destination path to copy the source file * @param {?{parseOffsets:boolean}} options parseOffsets allows optional * offset markup parsing. File is written to the destination path * without offsets. Offset data is passed to the doneCallbacks of the * promise. * @return {$.Promise} A promise resolved when the file is copied to the * destination. */ function copyFileEntry(source, destination, options) { options = options || {}; var deferred = new $.Deferred(); // read the source file FileUtils.readAsText(source).done(function (text, modificationTime) { getRoot().done(function (nfs) { var offsets; // optionally parse offsets if (options.parseOffsets) { var parseInfo = parseOffsetsFromText(text); text = parseInfo.text; offsets = parseInfo.offsets; } // create the new File createTextFile(destination, text, _getFileSystem()).done(function (entry) { deferred.resolve(entry, offsets, text); }).fail(function (err) { deferred.reject(err); }); }); }).fail(function (err) { deferred.reject(err); }); return deferred.promise(); } /** * Copy a directory source to a destination * @param {!Directory} source Directory to copy * @param {!string} destination Destination path to copy the source directory to * @param {?{parseOffsets:boolean, infos:Object, removePrefix:boolean}}} options * parseOffsets - allows optional offset markup parsing. File is written to the * destination path without offsets. Offset data is passed to the * doneCallbacks of the promise. * infos - an optional Object used when parseOffsets is true. Offset * information is attached here, indexed by the file destination path. * removePrefix - When parseOffsets is true, set removePrefix true * to add a new key to the infos array that drops the destination * path root. * @return {$.Promise} A promise resolved when the directory and all it's * contents are copied to the destination or rejected immediately * upon the first error. */ function copyDirectoryEntry(source, destination, options) { options = options || {}; options.infos = options.infos || {}; var parseOffsets = options.parseOffsets || false, removePrefix = options.removePrefix || true, deferred = new $.Deferred(), destDir = _getFileSystem().getDirectoryForPath(destination); // create the destination folder destDir.create(function (err) { if (err && err !== FileSystemError.ALREADY_EXISTS) { deferred.reject(); return; } source.getContents(function (err, contents) { if (!err) { // copy all children of this directory var copyChildrenPromise = Async.doInParallel( contents, function copyChild(child) { var childDestination = destination + "/" + child.name, promise; if (child.isDirectory) { promise = copyDirectoryEntry(child, childDestination, options); } else { promise = copyFileEntry(child, childDestination, options); if (parseOffsets) { // save offset data for each file path promise.done(function (destinationEntry, offsets, text) { options.infos[childDestination] = { offsets : offsets, fileEntry : destinationEntry, text : text }; }); } } return promise; } ); copyChildrenPromise.then(deferred.resolve, deferred.reject); } else { deferred.reject(err); } }); }); deferred.always(function () { // remove destination path prefix if (removePrefix && options.infos) { var shortKey; Object.keys(options.infos).forEach(function (key) { shortKey = key.substr(destination.length + 1); options.infos[shortKey] = options.infos[key]; }); } }); return deferred.promise(); } /** * Copy a file or directory source path to a destination * @param {!string} source Path for the source file or directory to copy * @param {!string} destination Destination path to copy the source file or directory * @param {?{parseOffsets:boolean, infos:Object, removePrefix:boolean}}} options * parseOffsets - allows optional offset markup parsing. File is written to the * destination path without offsets. Offset data is passed to the * doneCallbacks of the promise. * infos - an optional Object used when parseOffsets is true. Offset * information is attached here, indexed by the file destination path. * removePrefix - When parseOffsets is true, set removePrefix true * to add a new key to the infos array that drops the destination * path root. * @return {$.Promise} A promise resolved when the directory and all it's * contents are copied to the destination or rejected immediately * upon the first error. */ function copyPath(source, destination, options) { var deferred = new $.Deferred(); resolveNativeFileSystemPath(source).done(function (entry) { var promise; if (entry.isDirectory) { promise = copyDirectoryEntry(entry, destination, options); } else { promise = copyFileEntry(entry, destination, options); } promise.then(deferred.resolve, function (err) { console.error(destination); deferred.reject(); }); }).fail(function () { deferred.reject(); }); return deferred.promise(); } /** * Set editor cursor position to the given offset then activate an inline editor. * @param {!Editor} editor * @param {!{line:number, ch:number}} offset * @return {$.Promise} a promise that will be resolved when an inline * editor is created or rejected when no inline editors are available. */ function toggleQuickEditAtOffset(editor, offset) { editor.setCursorPos(offset.line, offset.ch); return _testWindow.executeCommand(Commands.TOGGLE_QUICK_EDIT); } /** * Simulate a key event. * @param {Number} key Key code * @param (String) event Key event to simulate * @param {HTMLElement} element Element to receive event * @param {KeyboardEventInit} options Optional arguments for key event */ function simulateKeyEvent(key, event, element, options) { var doc = element.ownerDocument; if(typeof options === 'undefined') { options = { view: doc.defaultView, bubbles: true, cancelable: true, keyIdentifer: key }; } else { options.view = doc.defaultView; options.bubbles = true; options.cancelable = true; options.keyIdentifier = key; } var oEvent = new KeyboardEvent(event, options); if (event !== "keydown" && event !== "keyup" && event !== "keypress") { console.log("SpecRunnerUtils.simulateKeyEvent() - unsupported keyevent: " + event); return; } // Chromium Hack: need to override the 'which' property. // Note: this code is not designed to work in IE, Safari, // or other browsers. Well, maybe with Firefox. YMMV. Object.defineProperty(oEvent, 'keyCode', { get: function () { return this.keyCodeVal; } }); Object.defineProperty(oEvent, 'which', { get: function () { return this.keyCodeVal; } }); Object.defineProperty(oEvent, 'charCode', { get: function () { return this.keyCodeVal; } }); oEvent.keyCodeVal = key; if (oEvent.keyCode !== key) { console.log("SpecRunnerUtils.simulateKeyEvent() - keyCode mismatch: " + oEvent.keyCode); } element.dispatchEvent(oEvent); } function getTestWindow() { return _testWindow; } function setLoadExtensionsInTestWindow(doLoadExtensions) { _doLoadExtensions = doLoadExtensions; } /** * Change the size of an editor. The window size is not affected by this function. * CodeMirror will change it's size withing Brackets. * * @param {!Editor} editor - instance of Editor * @param {?number} width - the new width of the editor in pixel * @param {?number} height - the new height of the editor in pixel */ function resizeEditor(editor, width, height) { var oldSize = {}; if (editor) { var jquery = editor.getRootElement().ownerDocument.defaultView.$, $editorHolder = jquery('#editor-holder'), $content = jquery('.content'); // preserve old size oldSize.width = $editorHolder.width(); oldSize.height = $editorHolder.height(); if (width) { $content.width(width); $editorHolder.width(width); editor.setSize(width, null); // Update CM size } if (height) { $content.height(height); $editorHolder.height(height); editor.setSize(null, height); // Update CM size } editor.refreshAll(true); // update CM } return oldSize; } /** * Extracts the jasmine.log() and/or jasmine.expect() messages from the given result, * including stack traces if available. * @param {Object} result A jasmine result item (from results.getItems()). * @return {string} the error message from that item. */ function getResultMessage(result) { var message; if (result.type === 'log') { message = result.toString(); } else if (result.type === 'expect' && result.passed && !result.passed()) { message = result.message; if (result.trace.stack) { message = result.trace.stack; } } return message; } /** * Searches the DOM tree for text containing the given content. Useful for verifying * that data you expect to show up in the UI somewhere is actually there. * * @param {jQueryObject|Node} root The root element to search from. Can be either a jQuery object * or a raw DOM node. * @param {string} content The content to find. * @param {boolean} asLink If true, find the content in the href of an <a> tag, otherwise find it in text nodes. * @return true if content was found */ function findDOMText(root, content, asLink) { // Unfortunately, we can't just use jQuery's :contains() selector, because it appears that // you can't escape quotes in it. var i; if (root.jquery) { root = root.get(0); } if (!root) { return false; } else if (!asLink && root.nodeType === 3) { // text node return root.textContent.indexOf(content) !== -1; } else { if (asLink && root.nodeType === 1 && root.tagName.toLowerCase() === "a" && root.getAttribute("href") === content) { return true; } var children = root.childNodes; for (i = 0; i < children.length; i++) { if (findDOMText(children[i], content, asLink)) { return true; } } return false; } } /** * Patches ProjectManager.getAllFiles() in the given test window (for just the current it() block) so that it * includes one extra file in its results. The file need not actually exist on disk. * @param {!Window} testWindow Brackets popup window * @param {string} extraFilePath Absolute path for the extra result file */ function injectIntoGetAllFiles(testWindow, extraFilePath) { var ProjectManager = testWindow.brackets.test.ProjectManager, FileSystem = testWindow.brackets.test.FileSystem, origGetAllFiles = ProjectManager.getAllFiles; spyOn(ProjectManager, "getAllFiles").andCallFake(function () { var testResult = new testWindow.$.Deferred(); origGetAllFiles.apply(ProjectManager, arguments).done(function (result) { var dummyFile = FileSystem.getFileForPath(extraFilePath); var newResult = result.concat([dummyFile]); testResult.resolve(newResult); }).fail(function (error) { testResult.reject(error); }); return testResult; }); } /** * Counts the number of active specs in the current suite. Includes all * descendants. * @param {(jasmine.Suite|jasmine.Spec)} suiteOrSpec * @return {number} */ function countSpecs(suiteOrSpec) { var children = suiteOrSpec.children && typeof suiteOrSpec.children === "function" && suiteOrSpec.children(); if (Array.isArray(children)) { var childCount = 0; children.forEach(function (child) { childCount += countSpecs(child); }); return childCount; } if (jasmine.getEnv().specFilter(suiteOrSpec)) { return 1; } return 0; } /** * @private * Adds a new before all or after all function to the current suite. If requires it creates a new * object to store the before all and after all functions and a spec counter for the current suite. * @param {string} type "beforeFirst" or "afterLast" * @param {function} func The function to store */ function _addSuiteFunction(type, func) { var suiteId = (jasmine.getEnv().currentSuite || _rootSuite).id; if (!_testSuites[suiteId]) { _testSuites[suiteId] = { beforeFirst : [], afterLast : [], specCounter : null }; } _testSuites[suiteId][type].push(func); } /** * Utility for tests that need to open a window or do something before every test in a suite * @param {function} func */ window.beforeFirst = function (func) { _addSuiteFunction("beforeFirst", func); }; /** * Utility for tests that need to close a window or do something after every test in a suite * @param {function} func */ window.afterLast = function (func) { _addSuiteFunction("afterLast", func); }; /** * @private * Returns an array with the parent suites of the current spec with the top most suite last * @return {Array.<jasmine.Suite>} */ function _getParentSuites() { var suite = jasmine.getEnv().currentSpec.suite, suites = []; while (suite) { suites.push(suite); suite = suite.parentSuite; } return suites; } /** * @private * Calls each function in the given array of functions * @param {Array.<function>} functions */ function _callFunctions(functions) { var spec = jasmine.getEnv().currentSpec; functions.forEach(function (func) { func.apply(spec); }); } /** * Calls the before first functions for the parent suites of the current spec when is the first spec of the suite. */ function runBeforeFirst() { var suites = _getParentSuites().reverse(); // SpecRunner-scoped beforeFirst if (_testSuites[_rootSuite.id].beforeFirst) { _callFunctions(_testSuites[_rootSuite.id].beforeFirst); _testSuites[_rootSuite.id].beforeFirst = null; } // Iterate through all the parent suites of the current spec suites.forEach(function (suite) { // If we have functions for this suite and it was never called, initialize the spec counter if (_testSuites[suite.id] && _testSuites[suite.id].specCounter === null) { _callFunctions(_testSuites[suite.id].beforeFirst); _testSuites[suite.id].specCounter = countSpecs(suite); } }); } /** * @private * @return {boolean} True if the current spect is the last spec to be run */ function _isLastSpec() { return _unitTestReporter.activeSpecCompleteCount === _unitTestReporter.activeSpecCount - 1; } /** * Calls the after last functions for the parent suites of the current spec when is the last spec of the suite. */ function runAfterLast() { var suites = _getParentSuites(); // Iterate throught all the parent suites of the current spec suites.forEach(function (suite) { // If we have functions for this suite, reduce the spec counter if (_testSuites[suite.id] && _testSuites[suite.id].specCounter > 0) { _testSuites[suite.id].specCounter--; // If this was the last spec of the suite run the after last functions and remove it if (_testSuites[suite.id].specCounter === 0) { _callFunctions(_testSuites[suite.id].afterLast); delete _testSuites[suite.id]; } } }); // SpecRunner-scoped afterLast if (_testSuites[_rootSuite.id].afterLast && _isLastSpec()) { _callFunctions(_testSuites[_rootSuite.id].afterLast); _testSuites[_rootSuite.id].afterLast = null; } } // "global" custom matchers beforeEach(function () { this.addMatchers({ /** * Expects the given editor's selection to be a cursor at the given position (no range selected) */ toHaveCursorPosition: function (line, ch, ignoreSelection) { var editor = this.actual; var selection = editor.getSelection(); var notString = this.isNot ? "not " : ""; var start = selection.start; var end = selection.end; var selectionMoreThanOneCharacter = start.line !== end.line || start.ch !== end.ch; this.message = function () { var message = "Expected the cursor to " + notString + "be at (" + line + ", " + ch + ") but it was actually at (" + start.line + ", " + start.ch + ")"; if (!this.isNot && selectionMoreThanOneCharacter) { message += " and more than one character was selected."; } return message; }; var positionsMatch = start.line === line && start.ch === ch; // when adding the not operator, it's confusing to check both the size of the // selection and the position. We just check the position in that case. if (this.isNot || ignoreSelection) { return positionsMatch; } else { return !selectionMoreThanOneCharacter && positionsMatch; } } }); }); function setUnitTestReporter(reporter) { _unitTestReporter = reporter; } exports.TEST_PREFERENCES_KEY = TEST_PREFERENCES_KEY; exports.EDITOR_USE_TABS = EDITOR_USE_TABS; exports.EDITOR_SPACE_UNITS = EDITOR_SPACE_UNITS; exports.chmod = chmod; exports.remove = remove; exports.copy = copy; exports.rename = rename; exports.getTestRoot = getTestRoot; exports.getTestPath = getTestPath; exports.getTempDirectory = getTempDirectory; exports.createTempDirectory = createTempDirectory; exports.getBracketsSourceRoot = getBracketsSourceRoot; exports.makeAbsolute = makeAbsolute; exports.resolveNativeFileSystemPath = resolveNativeFileSystemPath; exports.createEditorInstance = createEditorInstance; exports.createMockDocument = createMockDocument; exports.createMockActiveDocument = createMockActiveDocument; exports.createMockElement = createMockElement; exports.createMockEditorForDocument = createMockEditorForDocument; exports.createMockEditor = createMockEditor; exports.createMockPane = createMockPane; exports.createTestWindowAndRun = createTestWindowAndRun; exports.closeTestWindow = closeTestWindow; exports.clickDialogButton = clickDialogButton; exports.destroyMockEditor = destroyMockEditor; exports.loadProjectInTestWindow = loadProjectInTestWindow; exports.openProjectFiles = openProjectFiles; exports.toggleQuickEditAtOffset = toggleQuickEditAtOffset; exports.createTextFile = createTextFile; exports.copyDirectoryEntry = copyDirectoryEntry; exports.copyFileEntry = copyFileEntry; exports.copyPath = copyPath; exports.deletePath = deletePath; exports.getTestWindow = getTestWindow; exports.simulateKeyEvent = simulateKeyEvent; exports.setLoadExtensionsInTestWindow = setLoadExtensionsInTestWindow; exports.getResultMessage = getResultMessage; exports.parseOffsetsFromText = parseOffsetsFromText; exports.findDOMText = findDOMText; exports.injectIntoGetAllFiles = injectIntoGetAllFiles; exports.countSpecs = countSpecs; exports.runBeforeFirst = runBeforeFirst; exports.runAfterLast = runAfterLast; exports.removeTempDirectory = removeTempDirectory; exports.setUnitTestReporter = setUnitTestReporter; exports.resizeEditor = resizeEditor; });