/*
 * Copyright (c) 2013 - 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.
 *
 */

/*jslint regexp: true */
/*global describe, it, expect, beforeEach, afterEach, waitsFor, runs, waitsForDone, spyOn, jasmine */
/*unittests: ExtensionManager*/

define(function (require, exports, module) {
    "use strict";

    require("thirdparty/jquery.mockjax.js");

    var _ = require("thirdparty/lodash");

    var ExtensionManager          = require("extensibility/ExtensionManager"),
        ExtensionManagerView      = require("extensibility/ExtensionManagerView").ExtensionManagerView,
        ExtensionManagerViewModel = require("extensibility/ExtensionManagerViewModel"),
        ExtensionManagerDialog    = require("extensibility/ExtensionManagerDialog"),
        InstallExtensionDialog    = require("extensibility/InstallExtensionDialog"),
        Package                   = require("extensibility/Package"),
        ExtensionLoader           = require("utils/ExtensionLoader"),
        SpecRunnerUtils           = require("spec/SpecRunnerUtils"),
        NativeApp                 = require("utils/NativeApp"),
        Dialogs                   = require("widgets/Dialogs"),
        CommandManager            = require("command/CommandManager"),
        Commands                  = require("command/Commands"),
        FileSystem                = require("filesystem/FileSystem"),
        Strings                   = require("strings"),
        StringUtils               = require("utils/StringUtils"),
        LocalizationUtils         = require("utils/LocalizationUtils"),
        PreferencesManager        = require("preferences/PreferencesManager"),
        mockRegistryText          = require("text!spec/ExtensionManager-test-files/mockRegistry.json"),
        mockRegistryThemesText    = require("text!spec/ExtensionManager-test-files/mockRegistryThemes.json"),
        mockRegistryForSearch     = require("text!spec/ExtensionManager-test-files/mockRegistryForSearch.json"),
        mockExtensionList         = require("text!spec/ExtensionManager-test-files/mockExtensionList.json"),
        mockRegistry;

    describe("ExtensionManager", function () {
        var mockId, mockSettings, origRegistryURL, origExtensionUrl, removedPath,
            view, model, fakeLoadDeferred, modelDisposed, disabledFilePath;

        beforeEach(function () {
            // Use fake URLs for the registry (useful if the registry isn't actually currently
            // configured).
            origRegistryURL = brackets.config.extension_registry;
            origExtensionUrl = brackets.config.extension_url;
            brackets.config.extension_registry = "http://fake-registry.com/registry.json";
            brackets.config.extension_url = "http://fake-repository.com/{0}/{0}-{1}.zip";

            // Return a canned registry when requested. Individual tests can override this
            // at any point before the request is actually made.
            mockRegistry = JSON.parse(mockRegistryText);
            mockSettings = {
                url: brackets.config.extension_registry,
                dataType: "json",
                contentType: "application/json",
                response: function () {
                    this.responseText = mockRegistry;
                }
            };
            spyOn(mockSettings, "response").andCallThrough();
            mockId = $.mockjax(mockSettings);

            // Set a fake path for user extensions.
            var mockPath = SpecRunnerUtils.getTestPath("/spec/ExtensionManager-test-files");
            spyOn(ExtensionLoader, "getUserExtensionPath").andCallFake(function () {
                return mockPath + "/user";
            });

            // Fake package removal.
            removedPath = null;
            spyOn(Package, "remove").andCallFake(function (path) {
                removedPath = path;
                return new $.Deferred().resolve().promise();
            });

            // Fake enabling/disabling
            disabledFilePath = null;
            spyOn(Package, "disable").andCallFake(function (path) {
                disabledFilePath = path + "/.disabled";
                return new $.Deferred().resolve().promise();
            });
            spyOn(Package, "enable").andCallFake(function (path) {
                disabledFilePath = path + "/.disabled";
                return new $.Deferred().resolve().promise();
            });
        });

        afterEach(function () {
            $.mockjaxClear(mockId);
            ExtensionManager._reset();
            ExtensionManager.off(".unit-test");
            brackets.config.extension_registry = origRegistryURL;
            brackets.config.extension_url = origExtensionUrl;
        });

        function mockLoadExtensions(names, fail) {
            var numStatusChanges = 0,
                shouldFail = false,
                shouldDisable = false;
            if (typeof fail === "boolean") {
                shouldFail = true;
            } else if (typeof fail === "string") {
                shouldDisable = true;
            }
            runs(function () {
                ExtensionManager.on("statusChange.mock-load", function () {
                    numStatusChanges++;
                });
                var mockPath = SpecRunnerUtils.getTestPath("/spec/ExtensionManager-test-files");
                names = names || ["default/mock-extension-1", "dev/mock-extension-2", "user/mock-legacy-extension"];
                names.forEach(function (name) {
                    ExtensionLoader.trigger(shouldFail ? "loadFailed" : (shouldDisable ? "disabled" : "load"), mockPath + "/" + name);
                });
            });

            // Make sure the ExtensionManager has finished reading all the package.jsons before continuing.
            waitsFor(function () { return numStatusChanges === names.length; }, "ExtensionManager status changes");

            runs(function () {
                ExtensionManager.off(".mock-load");
            });
        }

        function makeMockInstalledVersion(mockRegistryExtension, installedVersion) {
            var ref = _.find(mockRegistryExtension.versions, { version: installedVersion });
            return {
                locationType: ExtensionManager.LOCATION_USER,
                metadata: {
                    name: mockRegistryExtension.metadata.name,
                    title: mockRegistryExtension.metadata.title,
                    version: ref.version,
                    engines: { brackets: ref.brackets }
                },
                owner: mockRegistryExtension.owner
            };
        }

        function makeMockExtension(versionRequirements) {
            var FAKE_DATE = "2013-04-10T18:28:20.530Z",
                versions = [];
            versionRequirements.forEach(function (verReq, i) {
                versions.push({ version: (i + 1) + ".0.0", brackets: verReq, published: FAKE_DATE });
            });
            var latestVer = versions[versions.length - 1];
            return {
                metadata: {
                    name: "mock-extension",
                    title: "Mock Extension",
                    version: latestVer.version,
                    engines: { brackets: latestVer.brackets }
                },
                owner: "github:someuser",
                versions: versions
            };
        }

        function setupExtensionManagerViewTests(context) {
            context.addMatchers({
                toHaveText: function (expected) {
                    var notText = this.isNot ? " not" : "";
                    this.message = function () {
                        return "Expected view" + notText + " to contain text " + expected;
                    };
                    return SpecRunnerUtils.findDOMText(this.actual.$el, expected);
                },
                toHaveLink: function (expected) {
                    var notText = this.isNot ? " not" : "";
                    this.message = function () {
                        return "Expected view" + notText + " to contain link " + expected;
                    };
                    return SpecRunnerUtils.findDOMText(this.actual.$el, expected, true);
                }
            });
            spyOn(InstallExtensionDialog, "installUsingDialog").andCallFake(function (url) {
                var id = url.match(/fake-repository\.com\/([^\/]+)/)[1];
                mockLoadExtensions(["user/" + id]);
            });
        }

        function cleanupExtensionManagerViewTests() {
            if (view) {
                view.$el.remove();
                view = null;
            }
            if (model) {
                model.dispose();
            }
        }

        // Sets up the view using the normal (mock) ExtensionManager data.
        function setupViewWithMockData(ModelClass) {
            runs(function () {
                view = new ExtensionManagerView();
                model = new ModelClass();
                modelDisposed = false;
                waitsForDone(view.initialize(model), "view initializing");
                view.$el.appendTo(window.document.body);
            });
            runs(function () {
                spyOn(view.model, "dispose").andCallThrough();
            });
        }

        describe("ExtensionManager", function () {
            it("should download the extension list from the registry", function () {
                runs(function () {
                    waitsForDone(ExtensionManager.downloadRegistry(), "fetching registry");
                });

                runs(function () {
                    expect(mockSettings.response).toHaveBeenCalled();
                    Object.keys(ExtensionManager.extensions).forEach(function (id) {
                        expect(ExtensionManager.extensions[id].registryInfo).toEqual(mockRegistry[id]);
                    });
                });
            });

            it("should trigger a registryUpdate event when updating the extension list from the registry", function () {
                var registryUpdateSpy;
                runs(function () {
                    registryUpdateSpy = jasmine.createSpy();
                    ExtensionManager.on("registryUpdate", registryUpdateSpy);
                    waitsForDone(ExtensionManager.downloadRegistry(), "fetching registry");
                });
                mockLoadExtensions();
                runs(function () {
                    expect(registryUpdateSpy).toHaveBeenCalled();
                });
            });

            it("should fail if it can't access the registry", function () {
                var gotDone = false, gotFail = false;
                runs(function () {
                    $.mockjaxClear(mockId);
                    mockId = $.mockjax({
                        url: brackets.config.extension_registry,
                        isTimeout: true
                    });
                    ExtensionManager.downloadRegistry()
                        .done(function () {
                            gotDone = true;
                        })
                        .fail(function () {
                            gotFail = true;
                        });
                });
                waitsFor(function () { return gotDone || gotFail; }, "mock failure");

                runs(function () {
                    expect(gotFail).toBe(true);
                    expect(gotDone).toBe(false);
                });
            });

            it("should fail if registry content is malformed", function () {
                var gotDone = false, gotFail = false;
                runs(function () {
                    mockRegistry = "{malformed json";
                    ExtensionManager.downloadRegistry()
                        .done(function () {
                            gotDone = true;
                        })
                        .fail(function () {
                            gotFail = true;
                        });
                });
                waitsFor(function () { return gotDone || gotFail; }, "bad mock data");

                runs(function () {
                    expect(gotFail).toBe(true);
                    expect(gotDone).toBe(false);
                });
            });

            it("should correctly list which extensions are installed", function () {
                runs(function () {
                    waitsForDone(ExtensionManager.downloadRegistry(), "loading registry");
                });
                mockLoadExtensions();
                runs(function () {
                    Object.keys(mockRegistry).forEach(function (extId) {
                        if (extId === "mock-extension-1" || extId === "mock-extension-2") {
                            expect(ExtensionManager.extensions[extId].installInfo.status).toEqual(ExtensionManager.ENABLED);
                        } else {
                            expect(ExtensionManager.extensions[extId].installInfo).toBeUndefined();
                        }
                    });
                });
            });

            it("should list an extension that is installed but failed to load", function () {
                runs(function () {
                    waitsForDone(ExtensionManager.downloadRegistry(), "loading registry");
                });
                mockLoadExtensions(["user/mock-extension-3"], true);
                runs(function () {
                    expect(ExtensionManager.extensions["mock-extension-3"].installInfo.status).toEqual(ExtensionManager.START_FAILED);
                });
            });

            it("should list an extension that is installed but disabled", function () {
                runs(function () {
                    waitsForDone(ExtensionManager.downloadRegistry(), "loading registry");
                });
                mockLoadExtensions(["user/mock-extension-3"], "disabled");
                runs(function () {
                    expect(ExtensionManager.extensions["mock-extension-3"].installInfo.status).toEqual(ExtensionManager.DISABLED);
                });
            });

            it("should set the title for a legacy extension based on its folder name", function () {
                mockLoadExtensions();
                runs(function () {
                    expect(ExtensionManager.extensions["mock-legacy-extension"].installInfo.metadata.title).toEqual("mock-legacy-extension");
                });
            });

            it("should determine the location type for installed extensions", function () {
                mockLoadExtensions();
                runs(function () {
                    expect(ExtensionManager.extensions["mock-extension-1"].installInfo.locationType).toEqual(ExtensionManager.LOCATION_DEFAULT);
                    expect(ExtensionManager.extensions["mock-extension-2"].installInfo.locationType).toEqual(ExtensionManager.LOCATION_DEV);
                    expect(ExtensionManager.extensions["mock-legacy-extension"].installInfo.locationType).toEqual(ExtensionManager.LOCATION_USER);
                });
            });

            it("should raise a statusChange event when an extension is loaded", function () {
                var spy = jasmine.createSpy();
                runs(function () {
                    ExtensionManager.on("statusChange.unit-test", spy);
                    mockLoadExtensions(["default/mock-extension-1"]);
                });
                runs(function () {
                    expect(spy).toHaveBeenCalledWith(jasmine.any(Object), "mock-extension-1");
                });
            });

            it("should raise a statusChange event when a legacy extension is loaded, with its path as the id", function () {
                var spy = jasmine.createSpy();
                runs(function () {
                    ExtensionManager.on("statusChange.unit-test", spy);
                    mockLoadExtensions(["user/mock-legacy-extension"]);
                });
                runs(function () {
                    expect(spy).toHaveBeenCalledWith(jasmine.any(Object), "mock-legacy-extension");
                });
            });

            it("should remove an extension and raise a statusChange event", function () {
                var spy = jasmine.createSpy();
                runs(function () {
                    mockLoadExtensions(["user/mock-extension-3"]);
                });
                runs(function () {
                    ExtensionManager.on("statusChange.unit-test", spy);
                    waitsForDone(ExtensionManager.remove("mock-extension-3"));
                });
                runs(function () {
                    var mockPath = SpecRunnerUtils.getTestPath("/spec/ExtensionManager-test-files");
                    expect(removedPath).toBe(mockPath + "/user/mock-extension-3");
                    expect(spy).toHaveBeenCalledWith(jasmine.any(Object), "mock-extension-3");
                    expect(ExtensionManager.extensions["mock-extension-3"].installInfo).toBeFalsy();
                });
            });

            it("should disable an extension and raise a statusChange event", function () {
                var spy = jasmine.createSpy();
                runs(function () {
                    mockLoadExtensions(["user/mock-extension-3"]);
                });
                runs(function () {
                    ExtensionManager.on("statusChange.unit-test", spy);
                    waitsForDone(ExtensionManager.disable("mock-extension-3"));
                });
                runs(function () {
                    var mockPath = SpecRunnerUtils.getTestPath("/spec/ExtensionManager-test-files");
                    expect(disabledFilePath).toBe(mockPath + "/user/mock-extension-3" + "/.disabled");
                    expect(spy).toHaveBeenCalledWith(jasmine.any(Object), "mock-extension-3");
                    expect(ExtensionManager.extensions["mock-extension-3"].installInfo.status).toEqual(ExtensionManager.DISABLED);
                });
            });

            it("should enable an extension and raise a statusChange event", function () {
                var spy = jasmine.createSpy();
                runs(function () {
                    mockLoadExtensions(["user/mock-extension-2"], "disable");
                });
                runs(function () {
                    ExtensionManager.on("statusChange.unit-test", spy);
                    waitsForDone(ExtensionManager.enable("mock-extension-2"));
                });
                runs(function () {
                    var mockPath = SpecRunnerUtils.getTestPath("/spec/ExtensionManager-test-files");
                    expect(disabledFilePath).toBe(mockPath + "/user/mock-extension-2" + "/.disabled");
                    expect(spy).toHaveBeenCalledWith(jasmine.any(Object), "mock-extension-2");
                    expect(ExtensionManager.extensions["mock-extension-2"].installInfo.status).toEqual(ExtensionManager.ENABLED);
                });
            });

            it("should fail when trying to remove an extension that's not installed", function () {
                var finished = false;
                runs(function () {
                    ExtensionManager.remove("mock-extension-3")
                        .done(function () {
                            finished = true;
                            expect("tried to remove a nonexistent extension").toBe(false);
                        })
                        .fail(function () {
                            finished = true;
                        });
                });
                waitsFor(function () { return finished; }, "finish removal");
            });

            it("should fail when trying to disable an extension that's not installed", function () {
                var finished = false;
                runs(function () {
                    ExtensionManager.disable("mock-extension-3")
                        .done(function () {
                            finished = true;
                            expect("tried to disable a nonexistent extension").toBe(false);
                        })
                        .fail(function () {
                            finished = true;
                        });
                });
                waitsFor(function () { return finished; }, "finish disabling");
            });

            it("should fail when trying to enable an extension that's not installed", function () {
                var finished = false;
                runs(function () {
                    ExtensionManager.enable("mock-extension-3")
                        .done(function () {
                            finished = true;
                            expect("tried to enable a nonexistent extension").toBe(false);
                        })
                        .fail(function () {
                            finished = true;
                        });
                });
                waitsFor(function () { return finished; }, "finish enabling");
            });

            it("should calculate compatibility info for installed extensions", function () {
                function fakeEntry(version) {
                    return { metadata: { engines: { brackets: version } } };
                }

                // Missing version requirement data
                expect(ExtensionManager.getCompatibilityInfo({ metadata: {} }, "1.0.0"))
                    .toEqual({isCompatible: true, isLatestVersion: true});
                expect(ExtensionManager.getCompatibilityInfo(fakeEntry(null), "1.0.0"))
                    .toEqual({isCompatible: true, isLatestVersion: true});

                // With version requirement data
                expect(ExtensionManager.getCompatibilityInfo(fakeEntry(">0.5.0"), "0.6.0"))
                    .toEqual({isCompatible: true, isLatestVersion: true});
                expect(ExtensionManager.getCompatibilityInfo(fakeEntry(">0.6.0"), "0.6.0"))
                    .toEqual({isCompatible: false, requiresNewer: true});
                expect(ExtensionManager.getCompatibilityInfo(fakeEntry(">0.7.0"), "0.6.0"))
                    .toEqual({isCompatible: false, requiresNewer: true});
                expect(ExtensionManager.getCompatibilityInfo(fakeEntry("<0.5.0"), "0.4.0"))
                    .toEqual({isCompatible: true, isLatestVersion: true});
                expect(ExtensionManager.getCompatibilityInfo(fakeEntry("<0.4.0"), "0.4.0"))
                    .toEqual({isCompatible: false, requiresNewer: false});
                expect(ExtensionManager.getCompatibilityInfo(fakeEntry("<0.3.0"), "0.4.0"))
                    .toEqual({isCompatible: false, requiresNewer: false});
                expect(ExtensionManager.getCompatibilityInfo(fakeEntry("~1.2"), "1.2.0"))
                    .toEqual({isCompatible: true, isLatestVersion: true});
                expect(ExtensionManager.getCompatibilityInfo(fakeEntry("~1.2"), "1.2.1"))
                    .toEqual({isCompatible: true, isLatestVersion: true});
                expect(ExtensionManager.getCompatibilityInfo(fakeEntry("~1.2"), "1.3.0"))
                    .toEqual({isCompatible: false, requiresNewer: false});
                expect(ExtensionManager.getCompatibilityInfo(fakeEntry("~1.2"), "1.3.1"))
                    .toEqual({isCompatible: false, requiresNewer: false});
                expect(ExtensionManager.getCompatibilityInfo(fakeEntry("~1.2"), "1.1.0"))
                    .toEqual({isCompatible: false, requiresNewer: true});
            });

            it("should calculate compatibility info for registry extensions", function () {
                // Use the fakeEntry name for consistency with the tests above
                var fakeEntry = makeMockExtension;

                var curVer = "0.33.0";
                expect(ExtensionManager.getCompatibilityInfo(fakeEntry([">=0.24"]), curVer))
                    .toEqual({isCompatible: true, compatibleVersion: "1.0.0", isLatestVersion: true});
                expect(ExtensionManager.getCompatibilityInfo(fakeEntry([">=0.24", ">=0.33"]), curVer))
                    .toEqual({isCompatible: true, compatibleVersion: "2.0.0", isLatestVersion: true});
                expect(ExtensionManager.getCompatibilityInfo(fakeEntry(["<=0.24", "<=0.29", ">=0.30"]), curVer))
                    .toEqual({isCompatible: true, compatibleVersion: "3.0.0", isLatestVersion: true});
                expect(ExtensionManager.getCompatibilityInfo(fakeEntry([">=0.40", ">=0.50", ">=0.30"]), curVer))
                    .toEqual({isCompatible: true, compatibleVersion: "3.0.0", isLatestVersion: true});
                expect(ExtensionManager.getCompatibilityInfo(fakeEntry(["<=0.29", "<=0.29"]), curVer))
                    .toEqual({isCompatible: false, requiresNewer: false});
                expect(ExtensionManager.getCompatibilityInfo(fakeEntry([">=0.40", ">=0.50"]), curVer))
                    .toEqual({isCompatible: false, requiresNewer: true});
                expect(ExtensionManager.getCompatibilityInfo(fakeEntry([">=0.15", ">=0.17", "<=0.20"]), curVer))
                    .toEqual({isCompatible: true, compatibleVersion: "2.0.0", isLatestVersion: false, requiresNewer: false});
                expect(ExtensionManager.getCompatibilityInfo(fakeEntry([">=0.24", ">=0.29", ">=0.50"]), curVer))
                    .toEqual({isCompatible: true, compatibleVersion: "2.0.0", isLatestVersion: false, requiresNewer: true});
                expect(ExtensionManager.getCompatibilityInfo(fakeEntry(["<=0.20", ">=0.30", ">=0.50"]), curVer))
                    .toEqual({isCompatible: true, compatibleVersion: "2.0.0", isLatestVersion: false, requiresNewer: true});
                expect(ExtensionManager.getCompatibilityInfo(fakeEntry([">=0.50", ">=0.30", "<=0.20"]), curVer))
                    .toEqual({isCompatible: true, compatibleVersion: "2.0.0", isLatestVersion: false, requiresNewer: false});
                expect(ExtensionManager.getCompatibilityInfo(fakeEntry([">=0.25", "<=0.40", ">=0.40", "<=0.40", ">=0.42"]), curVer))
                    .toEqual({isCompatible: true, compatibleVersion: "4.0.0", isLatestVersion: false, requiresNewer: true});
                expect(ExtensionManager.getCompatibilityInfo(fakeEntry([">=0.25", "<=0.30", ">=0.30", "<=0.40", ">=0.32"]), curVer))
                    .toEqual({isCompatible: true, compatibleVersion: "5.0.0", isLatestVersion: true});
                expect(ExtensionManager.getCompatibilityInfo(fakeEntry([">=0.25", ">=0.26", ">=0.30", ">=0.32", ">=0.40", ">=0.50"]), curVer))
                    .toEqual({isCompatible: true, compatibleVersion: "4.0.0", isLatestVersion: false, requiresNewer: true});
            });

            it("should return the correct download URL for an extension", function () {
                expect(ExtensionManager.getExtensionURL("my-cool-extension", "1.2.3"))
                    .toBe("http://fake-repository.com/my-cool-extension/my-cool-extension-1.2.3.zip");
            });
        });

        describe("Auto-Install Extensions", function () {

            function lookupFileName(zipArray, fileName) {
                return zipArray.some(function (zip) {
                    return (zip.file.name === fileName);
                });
            }

            function addZipFilesToArray(array, result) {
                result.installZips.every(function (zip) {
                    array[zip.info.metadata.name] = zip.info.metadata.version;
                });
                result.updateZips.every(function (zip) {
                    array[zip.info.metadata.name] = zip.info.metadata.version;
                });
            }

            it("should correctly handle auto-install extension zip files", function () {
                var promiseFail    = false,
                    promiseResult  = null,
                    autoExtensions = {},
                    dirPath        = SpecRunnerUtils.getTestPath("/spec/ExtensionManager-test-files/auto-install-extensions1");

                // Should find 1 install zip
                runs(function () {
                    ExtensionManager._getAutoInstallFiles(dirPath, autoExtensions)
                        .done(function (result) {
                            addZipFilesToArray(autoExtensions, result);
                            promiseResult = result;
                        })
                        .fail(function (err) {
                            promiseFail = true;
                            expect("[_getAutoInstallFiles] promise rejected with: " + err).toBe("(expected resolved instead)");
                        });
                });

                waitsFor(function () {
                    return promiseResult || promiseFail;
                }, "_getAutoInstallFiles success [_getAutoInstallFiles]", 1000);

                runs(function () {
                    expect(promiseResult).toBeTruthy();
                    if (promiseResult) {
                        expect(promiseResult.installZips.length).toBe(1);
                        expect(promiseResult.updateZips.length).toBe(0);

                        // Extension
                        expect(lookupFileName(promiseResult.installZips, "mock-extension-v1.0.0.zip")).toBeTruthy();

                        // Not extensions
                        expect(lookupFileName(promiseResult.installZips, "ignore-this-folder")).toBeFalsy();
                        expect(lookupFileName(promiseResult.installZips, "not-an-extension.zip")).toBeFalsy();
                        expect(lookupFileName(promiseResult.installZips, "should-be-ignored.txt")).toBeFalsy();
                    }
                });

                // Subsequent run of first folder should find 0 install zips
                runs(function () {
                    promiseFail   = false;
                    promiseResult = null;
                    ExtensionManager._getAutoInstallFiles(dirPath, autoExtensions)
                        .done(function (result) {
                            addZipFilesToArray(autoExtensions, result);
                            promiseResult = result;
                        })
                        .fail(function (err) {
                            promiseFail = true;
                            expect("[_getAutoInstallFiles] promise rejected with: " + err).toBe("(expected resolved instead)");
                        });
                });

                waitsFor(function () {
                    return promiseResult || promiseFail;
                }, "_getAutoInstallFiles success [_getAutoInstallFiles]", 1000);

                runs(function () {
                    expect(promiseResult).toBeTruthy();
                    if (promiseResult) {
                        expect(promiseResult.installZips.length).toBe(0);
                        expect(promiseResult.updateZips.length).toBe(0);

                        // Extension already installed
                        expect(lookupFileName(promiseResult.installZips, "mock-extension-v1.0.0.zip")).toBeFalsy();

                        // Not extensions
                        expect(lookupFileName(promiseResult.installZips, "ignore-this-folder")).toBeFalsy();
                        expect(lookupFileName(promiseResult.installZips, "not-an-extension.zip")).toBeFalsy();
                        expect(lookupFileName(promiseResult.installZips, "should-be-ignored.txt")).toBeFalsy();
                    }
                });

            });

            it("should detect auto-install extension is an update", function () {
                var promiseFail    = false,
                    promiseResult  = null,
                    autoExtensions = {},
                    dirPath        = SpecRunnerUtils.getTestPath("/spec/ExtensionManager-test-files/auto-install-extensions2");

                runs(function () {
                    mockRegistry = { "mock-extension": makeMockExtension([">0.1", ">0.1"]) };
                    var mockInstallInfo = { "mock-extension": { installInfo: makeMockInstalledVersion(mockRegistry["mock-extension"], "1.0.0") } };
                    ExtensionManager._setExtensions(mockInstallInfo);

                    ExtensionManager._getAutoInstallFiles(dirPath, autoExtensions)
                        .done(function (result) {
                            addZipFilesToArray(autoExtensions, result);
                            promiseResult = result;
                        })
                        .fail(function (err) {
                            promiseFail = true;
                            expect("[_getAutoInstallFiles] promise rejected with: " + err).toBe("(expected resolved instead)");
                        });
                });

                waitsFor(function () {
                    return promiseResult || promiseFail;
                }, "_getAutoInstallFiles success [_getAutoInstallFiles]", 1000);

                runs(function () {
                    expect(promiseResult).toBeTruthy();
                    if (promiseResult) {
                        expect(promiseResult.installZips.length).toBe(0);
                        expect(promiseResult.updateZips.length).toBe(1);

                        // Extension
                        expect(lookupFileName(promiseResult.updateZips, "mock-extension-v1.1.1.zip")).toBeTruthy();
                    }
                });
            });

            it("should correctly handle multiple auto-install extension zip files of same extension", function () {
                var promiseFail    = false,
                    promiseResult  = null,
                    autoExtensions = {},
                    dirPath        = SpecRunnerUtils.getTestPath("/spec/ExtensionManager-test-files/auto-install-extensions3");

                // There are 3 zips of same extension using 3 different versions. Only the latest should be returned.
                runs(function () {
                    ExtensionManager._getAutoInstallFiles(dirPath, autoExtensions)
                        .done(function (result) {
                            addZipFilesToArray(autoExtensions, result);
                            promiseResult = result;
                        })
                        .fail(function (err) {
                            promiseFail = true;
                            expect("[_getAutoInstallFiles] promise rejected with: " + err).toBe("(expected resolved instead)");
                        });
                });

                waitsFor(function () {
                    return promiseResult || promiseFail;
                }, "_getAutoInstallFiles success [_getAutoInstallFiles]", 1000);

                runs(function () {
                    expect(promiseResult).toBeTruthy();
                    if (promiseResult) {
                        expect(promiseResult.installZips.length).toBe(1);
                        expect(promiseResult.updateZips.length).toBe(0);

                        // Latest version
                        // Note that files are named to try to force code to hit desired branches, but order is arbitrary.
                        expect(lookupFileName(promiseResult.installZips, "b-mock-extension-v1.1.1.zip")).toBeTruthy();

                        // Older versions
                        expect(lookupFileName(promiseResult.installZips, "a-mock-extension-v1.0.0.zip")).toBeFalsy();
                        expect(lookupFileName(promiseResult.installZips, "c-mock-extension-v1.1.0.zip")).toBeFalsy();
                    }
                });
            });
        });

        describe("ExtensionManagerView Model", function () {
            describe("when initialized from registry", function () {
                var model;

                beforeEach(function () {
                    runs(function () {
                        mockRegistry = JSON.parse(mockRegistryForSearch);
                        model = new ExtensionManagerViewModel.RegistryViewModel();
                        waitsForDone(model.initialize(), "model initialization");
                    });
                    runs(function () {
                        // Mock load some extensions, so we can make sure they don't show up in the filtered model in this case.
                        mockLoadExtensions();
                    });
                });

                afterEach(function () {
                    model.dispose();
                    model = null;
                });

                it("should initialize itself from the extension list", function () {
                    expect(model.extensions).toEqual(ExtensionManager.extensions);
                });

                it("should start with the full set sorted in reverse download count order", function () {
                    PreferencesManager.set("extensions.sort", "downloadCount");
                    model._setSortedExtensionList(ExtensionManager.extensions, false);
                    expect(model.filterSet).toEqual(["item-6", "item-4", "item-3", "find-uniq1-in-name", "item-2", "item-5"]);
                });
                
                it("should start with the full set sorted in reverse publish date order", function () {
                    PreferencesManager.set("extensions.sort", "publishedDate");
                    model._setSortedExtensionList(ExtensionManager.extensions, false);
                    expect(model.filterSet).toEqual(["item-5", "item-6", "item-2", "find-uniq1-in-name", "item-4", "item-3"]);
                });

                it("should search case-insensitively for a keyword in the metadata for a given list of registry ids", function () {
                    model.filter("uniq1");
                    expect(model.filterSet).toEqual(["find-uniq1-in-name"]);
                    model.filter("uniq2");
                    expect(model.filterSet).toEqual(["item-2"]);
                    model.filter("uniq3");
                    expect(model.filterSet).toEqual(["item-3"]);
                    model.filter("uniq4");
                    expect(model.filterSet).toEqual(["item-4"]);
                    model.filter("uniq5");
                    expect(model.filterSet).toEqual(["item-5"]);
                    model.filter("uniq6");
                    expect(model.filterSet).toEqual(["item-6"]);
                    model.filter("uniqin1and5");
                    expect(model.filterSet).toEqual(["item-5", "find-uniq1-in-name"]); // sorted in reverse publish date order
                });

                it("should 'AND' space-separated search terms", function () {
                    model.filter("UNIQ2 in author name");
                    expect(model.filterSet).toEqual(["item-2"]);
                    model.filter("UNIQ2 name");
                    expect(model.filterSet).toEqual(["item-2"]);
                    model.filter("UNIQ2 name author");
                    expect(model.filterSet).toEqual(["item-2"]);
                    model.filter("UNIQ2 uniq3");
                    expect(model.filterSet).toEqual([]);
                });

                it("should return correct results when subsequent queries are longer versions of previous queries", function () {
                    model.filter("uniqin1and5");
                    model.filter("uniqin1and5-2");
                    expect(model.filterSet).toEqual(["item-5"]);
                });

                it("should go back to the full sorted set when cleared", function () {
                    model.filter("uniq1");
                    model.filter("");
                    expect(model.filterSet).toEqual(["item-5", "item-6", "item-2", "find-uniq1-in-name", "item-4", "item-3"]);
                });

                it("longer versions of previous queries, and not, should also work with spaces", function () {
                    model.filter("name");
                    expect(model.filterSet).toEqual(["item-2", "find-uniq1-in-name"]);
                    model.filter("name uniq");
                    expect(model.filterSet).toEqual(["item-2", "find-uniq1-in-name"]);
                    model.filter("name uniq2");
                    expect(model.filterSet).toEqual(["item-2"]);
                    model.filter("name uniq");
                    expect(model.filterSet).toEqual(["item-2", "find-uniq1-in-name"]);
                    model.filter("name");
                    expect(model.filterSet).toEqual(["item-2", "find-uniq1-in-name"]);
                });

                it("should trigger filter event when filter changes", function () {
                    var gotEvent = false;
                    model.on("filter", function () {
                        gotEvent = true;
                    });
                    model.filter("uniq1");
                    expect(gotEvent).toBe(true);
                });
            });


            describe("when initialized themes from registry", function () {
                var model;

                beforeEach(function () {
                    runs(function () {
                        mockRegistry = JSON.parse(mockRegistryThemesText);
                        model = new ExtensionManagerViewModel.ThemesViewModel();
                        waitsForDone(model.initialize(), "model initialization");
                    });
                    runs(function () {
                        // Mock load some extensions, so we can make sure they don't show up in the filtered model in this case.
                        mockLoadExtensions();
                    });
                });

                afterEach(function () {
                    model.dispose();
                    model = null;
                });

                it("should initialize itself from the extension list", function () {
                    expect(model.extensions).toEqual(ExtensionManager.extensions);
                });

                it("should start with the full set sorted in reverse publish date order", function () {
                    expect(model.filterSet).toEqual(["theme-1", "theme-2"]);
                });
                
                it("should start with the full set sorted in reverse download count order", function () {
                    PreferencesManager.set("extensions.sort", "downloadCount");
                    model._setSortedExtensionList(ExtensionManager.extensions, true);
                    expect(model.filterSet).toEqual(["theme-2", "theme-1"]);
                });
            });


            describe("when initialized from local extension list", function () {
                var model, origExtensions;

                beforeEach(function () {
                    runs(function () {
                        origExtensions = ExtensionManager.extensions;
                        ExtensionManager._setExtensions(JSON.parse(mockExtensionList));
                        model = new ExtensionManagerViewModel.InstalledViewModel();
                        waitsForDone(model.initialize());
                    });
                });

                afterEach(function () {
                    model.dispose();
                    model = null;
                    ExtensionManager._setExtensions(origExtensions);
                });

                it("should initialize itself from the extension list", function () {
                    expect(model.extensions).toEqual(ExtensionManager.extensions);
                });

                it("should only contain dev and user extensions, sorted case-insensitively on the extension title or name (or last segment of path name for legacy extensions)", function () {
                    expect(model.filterSet).toEqual(["registered-extension", "dev-extension", "/path/to/extensions/user/legacy-extension", "unregistered-extension", "Z-capital-extension"]);
                });

                it("should include a newly-installed extension", function () {
                    mockLoadExtensions(["user/install-later-extension"]);
                    runs(function () {
                        expect(model.filterSet.indexOf("install-later-extension")).toBe(2);
                    });
                });

                it("should include a newly-installed disabled extension", function () {
                    mockLoadExtensions(["user/another-great-extension"], "disabled");
                    runs(function () {
                        expect(model.filterSet.indexOf("another-great-extension")).toBe(1);
                    });
                });

                it("should raise an event when an extension is installed", function () {
                    var calledId;
                    runs(function () {
                        model.on("change", function (e, id) {
                            calledId = id;
                        });
                    });
                    mockLoadExtensions(["user/install-later-extension"]);
                    runs(function () {
                        expect(calledId).toBe("install-later-extension");
                    });
                });

                it("should raise an event when an extension is disabled", function () {
                    var calledId;
                    runs(function () {
                        model.on("change", function (e, id) {
                            calledId = id;
                        });
                    });
                    mockLoadExtensions(["user/another-great-extension"], "disabled");
                    runs(function () {
                        expect(calledId).toBe("another-great-extension");
                    });
                });

                it("should not include a removed extension", function () {
                    runs(function () {
                        waitsForDone(ExtensionManager.remove("registered-extension"));
                    });
                    runs(function () {
                        expect(model.filterSet.indexOf("registered-extension")).toBe(-1);
                    });
                });

                it("should raise an event when an extension is removed", function () {
                    var calledId;
                    runs(function () {
                        model.on("change", function (e, id) {
                            calledId = id;
                        });
                        waitsForDone(ExtensionManager.remove("registered-extension"));
                    });
                    runs(function () {
                        expect(calledId).toBe("registered-extension");
                    });
                });

                it("should mark an extension for removal and raise an event without actually removing it", function () {
                    var id = "registered-extension", calledId;
                    runs(function () {
                        model.on("change", function (e, id) {
                            calledId = id;
                        });
                        ExtensionManager.markForRemoval(id, true);
                        expect(calledId).toBe(id);
                        expect(ExtensionManager.isMarkedForRemoval(id)).toBe(true);
                        expect(model.filterSet.indexOf(id)).not.toBe(-1);
                        expect(ExtensionManager.hasExtensionsToRemove()).toBe(true);
                    });
                });

                it("should unmark an extension previously marked for removal and raise an event", function () {
                    var id = "registered-extension", calledId;
                    runs(function () {
                        ExtensionManager.markForRemoval(id, true);
                        model.on("change", function (e, id) {
                            calledId = id;
                        });
                        ExtensionManager.markForRemoval(id, false);
                        expect(calledId).toBe(id);
                        expect(ExtensionManager.isMarkedForRemoval(id)).toBe(false);
                        expect(ExtensionManager.hasExtensionsToRemove()).toBe(false);
                    });
                });

                it("should remove extensions previously marked for removal", function () {
                    var removedIds = {}, removedPaths = {};
                    runs(function () {
                        ExtensionManager.markForRemoval("registered-extension", true);
                        ExtensionManager.markForRemoval("Z-capital-extension", false);
                        model.on("change", function (e, id) {
                            removedIds[id] = true;
                            removedPaths[removedPath] = true;
                        });
                        waitsForDone(ExtensionManager.removeMarkedExtensions());
                    });
                    runs(function () {
                        // Test a removed extension, an extension that was unmarked for removal, and an extension that was never marked.
                        expect(removedIds["registered-extension"]).toBe(true);
                        expect(removedPaths["/path/to/extensions/user/registered-extension"]).toBe(true);
                        expect(removedIds["Z-capital-extension"]).toBeUndefined();
                        expect(removedPaths["/path/to/extensions/user/Z-capital-extension"]).toBeUndefined();
                        expect(removedIds["unregistered-extension"]).toBeUndefined();
                        expect(removedPaths["/path/to/extensions/user/unregistered-extension"]).toBeUndefined();
                    });
                });

                it("should mark an extension for disabling and raise an event", function () {
                    var id = "registered-extension", calledId;
                    runs(function () {
                        model.on("change", function (e, id) {
                            calledId = id;
                        });
                        ExtensionManager.markForDisabling(id, true);
                        expect(calledId).toBe(id);
                        expect(ExtensionManager.isMarkedForDisabling(id)).toBe(true);
                        expect(model.filterSet.indexOf(id)).not.toBe(-1);
                        expect(ExtensionManager.hasExtensionsToDisable()).toBe(true);
                    });
                });

                it("should unmark an extension previously marked for disabling and raise an event", function () {
                    var id = "registered-extension", calledId;
                    runs(function () {
                        ExtensionManager.markForDisabling(id, true);
                        model.on("change", function (e, id) {
                            calledId = id;
                        });
                        ExtensionManager.markForDisabling(id, false);
                        expect(calledId).toBe(id);
                        expect(ExtensionManager.isMarkedForRemoval(id)).toBe(false);
                        expect(ExtensionManager.hasExtensionsToRemove()).toBe(false);
                    });
                });

                it("should disable extensions previously marked for disabling and not remove them from the model", function () {
                    var disabledIds = {}, disabledPaths = {};
                    runs(function () {
                        ExtensionManager.markForDisabling("registered-extension", true);
                        ExtensionManager.markForDisabling("Z-capital-extension", false);
                        model.on("change", function (e, id) {
                            disabledIds[id] = true;
                            disabledPaths[disabledFilePath] = true;
                        });
                        waitsForDone(ExtensionManager.disableMarkedExtensions());
                    });
                    runs(function () {
                        // Test the enabled extension, the extension that was unmarked for disabling, and an extension that was never marked.
                        expect(disabledIds["registered-extension"]).toBe(true);
                        expect(disabledPaths["/path/to/extensions/user/registered-extension/.disabled"]).toBe(true);
                        expect(model.filterSet.indexOf("registered-extension")).toBe(0);
                        expect(disabledIds["Z-capital-extension"]).toBeUndefined();
                        expect(disabledPaths["/path/to/extensions/user/Z-capital-extension/.disabled"]).toBeUndefined();
                        expect(disabledIds["unregistered-extension"]).toBeUndefined();
                        expect(disabledPaths["/path/to/extensions/user/unregistered-extension/.disabled"]).toBeUndefined();
                    });
                });

                it("should delete the .disabled file, enable the extension and raise an event", function () {
                    var extension = "registered-extension",
                        calledId;
                    runs(function () {
                        mockLoadExtensions(["registered-extension"], "disabled");
                        model.on("change", function (e, id) {
                            calledId = id;
                        });
                    });
                    runs(function () {
                        waitsForDone(ExtensionManager.enable(extension));
                    });
                    runs(function () {
                        expect(calledId).toBe(extension);
                    });
                });

                it("should mark an extension for update and raise an event", function () {
                    var id = "registered-extension", calledId;
                    runs(function () {
                        model.on("change", function (e, id) {
                            calledId = id;
                        });
                        ExtensionManager.updateFromDownload({
                            localPath: "/path/to/downloaded/file.zip",
                            name: id,
                            installationStatus: "NEEDS_UPDATE"
                        });
                        expect(calledId).toBe(id);
                        expect(ExtensionManager.isMarkedForUpdate(id)).toBe(true);
                        expect(ExtensionManager.hasExtensionsToUpdate()).toBe(true);
                    });
                });

                it("should unmark an extension for update, deleting the package and raising an event", function () {
                    var id = "registered-extension",
                        filename = "/path/to/downloaded/file.zip",
                        file = FileSystem.getFileForPath(filename),
                        calledId;
                    runs(function () {
                        model.on("change", function (e, id) {
                            calledId = id;
                        });
                        ExtensionManager.updateFromDownload({
                            localPath: filename,
                            name: id,
                            installationStatus: "NEEDS_UPDATE"
                        });
                        calledId = null;
                        spyOn(file, "unlink");
                        ExtensionManager.removeUpdate(id);
                        expect(calledId).toBe(id);
                        expect(file.unlink).toHaveBeenCalled();
                        expect(ExtensionManager.isMarkedForUpdate()).toBe(false);
                    });
                });

                it("should change an extension marked for removal to update raise an event", function () {
                    var id = "registered-extension", calledId;
                    runs(function () {
                        model.on("change", function (e, id) {
                            calledId = id;
                        });
                        ExtensionManager.markForRemoval(id, true);
                        expect(calledId).toBe(id);
                        calledId = null;
                        ExtensionManager.updateFromDownload({
                            localPath: "/path/to/downloaded/file.zip",
                            name: id,
                            installationStatus: "NEEDS_UPDATE"
                        });
                        expect(calledId).toBe(id);
                        expect(ExtensionManager.isMarkedForRemoval()).toBe(false);
                        expect(ExtensionManager.hasExtensionsToRemove()).toBe(false);
                        expect(ExtensionManager.isMarkedForUpdate(id)).toBe(true);
                        expect(ExtensionManager.hasExtensionsToUpdate()).toBe(true);
                    });
                });

                it("should update extensions marked for update", function () {
                    var id = "registered-extension",
                        filename = "/path/to/downloaded/file.zip",
                        file = FileSystem.getFileForPath("/path/to/downloaded/file.zip");
                    runs(function () {
                        ExtensionManager.updateFromDownload({
                            localPath: filename,
                            name: id,
                            installationStatus: "NEEDS_UPDATE"
                        });
                        expect(ExtensionManager.isMarkedForUpdate()).toBe(false);
                        spyOn(file, "unlink");
                        var d = $.Deferred();
                        spyOn(Package, "installUpdate").andReturn(d.promise());
                        d.resolve();
                        waitsForDone(ExtensionManager.updateExtensions());
                    });
                    runs(function () {
                        expect(file.unlink).toHaveBeenCalled();
                        expect(Package.installUpdate).toHaveBeenCalledWith(filename, id);
                    });
                });

                it("should recognize when an update is available", function () {
                    var id = "registered-extension";
                    runs(function () {
                        console.log(model.extensions[id]);
                        expect(model._getEntry(id).updateAvailable).toBe(true);
                    });
                });
            });
        });

        describe("Local File Install", function () {
            var didReload;

            function clickOk() {
                var $okBtn = $(".install-extension-dialog.instance .dialog-button[data-button-id='ok']");
                $okBtn.click();
            }

            beforeEach(function () {
                // Mock reloading the app so we don't actually reload :)
                didReload = false;
                spyOn(CommandManager, "execute").andCallFake(function (id) {
                    if (id === Commands.APP_RELOAD) {
                        didReload = true;
                    } else {
                        CommandManager.execute.apply(this, arguments);
                    }
                });
            });

            it("should set flag to keep local files for new installs", function () {
                var filename = "/path/to/downloaded/file.zip",
                    file = FileSystem.getFileForPath(filename),
                    result;

                runs(function () {
                    spyOn(file, "unlink");

                    // Mock install
                    var d = $.Deferred().resolve({});
                    spyOn(Package, "installFromPath").andReturn(d.promise());

                    var promise = InstallExtensionDialog.installUsingDialog(file);
                    promise.done(function (_result) {
                        result = _result;
                    });

                    clickOk();
                    waitsForDone(promise);
                });

                runs(function () {
                    expect(Package.installFromPath).toHaveBeenCalledWith(filename);
                    expect(result.keepFile).toBe(true);
                    expect(file.unlink).not.toHaveBeenCalled();
                });
            });

            it("should set flag to keep local files for updates", function () {
                var id = "mock-extension",
                    filename = "/path/to/downloaded/file.zip",
                    file = FileSystem.getFileForPath(filename),
                    result,
                    dialogDeferred = new $.Deferred(),
                    $mockDlg,
                    didClose;

                // Mock update
                var installResult = {
                    installationStatus: Package.InstallationStatuses.NEEDS_UPDATE,
                    name: id,
                    localPath: filename
                };

                // Mock dialog
                spyOn(Dialogs, "showModalDialog").andCallFake(function () {
                    $mockDlg = $("<div/>");
                    didClose = false;

                    return {
                        getElement: function () { return $mockDlg; },
                        close: function () { didClose = true; }
                    };
                });

                spyOn(file, "unlink");

                // Mock installs to avoid calling into node ExtensionManagerDomain
                spyOn(Package, "installFromPath").andCallFake(function () {
                    return new $.Deferred().resolve(installResult).promise();
                });
                spyOn(Package, "installUpdate").andCallFake(function () {
                    return new $.Deferred().resolve(installResult).promise();
                });

                runs(function () {
                    // Mimic drag and drop
                    InstallExtensionDialog.updateUsingDialog(file)
                        .done(function (_result) {
                            result = _result;

                            // Mark for update
                            ExtensionManager.updateFromDownload(result);

                            dialogDeferred.resolve();
                        })
                        .fail(dialogDeferred.reject);

                    clickOk();
                    waitsForDone(dialogDeferred.promise(), "InstallExtensionDialog.updateUsingDialog");
                });

                runs(function () {
                    // InstallExtensionDialog should set keepFile=true
                    expect(result.keepFile).toBe(true);

                    // Run update, creates dialog DIALOG_ID_CHANGE_EXTENSIONS
                    ExtensionManagerDialog._performChanges();
                    $mockDlg.triggerHandler("buttonClick", Dialogs.DIALOG_BTN_OK);
                });

                waitsFor(function () {
                    return didClose;
                }, "DIALOG_ID_CHANGE_EXTENSIONS closed");

                runs(function () {
                    expect(file.unlink).not.toHaveBeenCalled();
                    expect(didReload).toBe(true);
                });
            });

        });

        describe("ExtensionManagerView", function () {

            beforeEach(function () {
                setupExtensionManagerViewTests(this);
                spyOn(brackets, "getLocale").andReturn("en");
            });


            afterEach(function () {
                cleanupExtensionManagerViewTests();
            });

            describe("when showing registry entries", function () {
                it("should populate itself with registry entries and display their fields when created", function () {
                    setupViewWithMockData(ExtensionManagerViewModel.RegistryViewModel);
                    runs(function () {
                        _.forEach(mockRegistry, function (item) {
                            // Should show the title if specified, otherwise the bare name.
                            if (item.metadata.title) {
                                expect(view).toHaveText(item.metadata.title);
                            } else {
                                expect(view).toHaveText(item.metadata.name);
                            }

                            // Simple fields
                            [item.metadata.version,
                                item.metadata.author && item.metadata.author.name,
                                item.totalDownloads]
                                .forEach(function (value) {
                                    if (value) {
                                        expect(view).toHaveText(value);
                                    }
                                });
                            if (item.metadata.homepage) {
                                expect(view).toHaveLink(item.metadata.homepage);
                            }

                            // Array-valued fields
                            [item.metadata.categories].forEach(function (arr) {
                                if (arr) {
                                    arr.forEach(function (value) {
                                        expect(view).toHaveText(value);
                                    });
                                }
                            });
                        });
                    });
                });

                it("should display original description", function () {
                    setupViewWithMockData(ExtensionManagerViewModel.RegistryViewModel);
                    runs(function () {
                        _.forEach(mockRegistry, function (item) {
                            if (item.metadata.description) {
                                if (StringUtils.truncate(item.metadata.description, 200) === undefined) {
                                    expect(view).toHaveText(item.metadata.description);
                                }
                            }
                        });
                    });
                });

                it("should display shortened description", function () {
                    setupViewWithMockData(ExtensionManagerViewModel.RegistryViewModel);
                    runs(function () {
                        _.forEach(mockRegistry, function (item) {
                            if (item.metadata.description) {
                                var shortDescription = StringUtils.truncate(item.metadata.description, 200);
                                if (shortDescription !== undefined) {
                                    expect(view).toHaveText(shortDescription);
                                }
                            }
                        });
                    });
                });

                it("should display owner even for installed items", function () {
                    ExtensionManager._setExtensions(JSON.parse(mockExtensionList));
                    setupViewWithMockData(ExtensionManagerViewModel.InstalledViewModel);
                    runs(function () {
                        console.log(view);
                        _.forEach(JSON.parse(mockExtensionList), function (item) {
                            if (item.installInfo && item.registryInfo) {
                                // Owner--should show only the owner name, not the authenticator
                                expect(view).toHaveText(item.registryInfo.owner.split(":")[1]);
                            }
                        });
                    });
                });

                it("should show an install button for each item", function () {
                    setupViewWithMockData(ExtensionManagerViewModel.RegistryViewModel);
                    runs(function () {
                        _.forEach(mockRegistry, function (item) {
                            var $button = $("button.install[data-extension-id=" + item.metadata.name + "]", view.$el);
                            expect($button.length).toBe(1);
                        });
                    });
                });

                // 'Install' button state
                it("should show disabled install buttons for items that are already installed", function () {
                    mockLoadExtensions(["user/mock-extension-3", "user/mock-extension-4"]);
                    setupViewWithMockData(ExtensionManagerViewModel.RegistryViewModel);
                    runs(function () {
                        _.forEach(mockRegistry, function (item) {
                            var $button = $("button.install[data-extension-id=" + item.metadata.name + "]", view.$el);
                            if (item.metadata.name === "mock-extension-3" || item.metadata.name === "mock-extension-4") {
                                expect($button.prop("disabled")).toBeTruthy();
                            } else {
                                expect($button.prop("disabled")).toBeFalsy();
                            }
                        });
                    });
                });

                it("should show disabled install button if requires newer API version", function () {   // isCompatible: false, requiresNewer: true
                    runs(function () {
                        mockRegistry = { "mock-extension": makeMockExtension([">100.0"]) };
                        setupViewWithMockData(ExtensionManagerViewModel.RegistryViewModel);
                    });
                    runs(function () {
                        var $button = $("button.install[data-extension-id=mock-extension]", view.$el);
                        expect($button.length).toBe(1);
                        expect($button.prop("disabled")).toBeTruthy();
                        expect($("button.update[data-extension-id=mock-extension]", view.$el).length).toBe(0);

                        var $warning = $(".alert.warning", view.$el);
                        expect($warning.length).toBe(1);
                        expect($warning.text().trim()).toBe(Strings.EXTENSION_INCOMPATIBLE_NEWER);
                    });
                });

                it("should show disabled install button if requires older API version", function () {   // isCompatible: false, requiresNewer: false
                    runs(function () {
                        mockRegistry = { "mock-extension": makeMockExtension(["<0.1"]) };
                        setupViewWithMockData(ExtensionManagerViewModel.RegistryViewModel);
                    });
                    runs(function () {
                        var $button = $("button.install[data-extension-id=mock-extension]", view.$el);
                        expect($button.length).toBe(1);
                        expect($button.prop("disabled")).toBeTruthy();
                        expect($("button.update[data-extension-id=mock-extension]", view.$el).length).toBe(0);

                        var $warning = $(".alert.warning", view.$el);
                        expect($warning.length).toBe(1);
                        expect($warning.text().trim()).toBe(Strings.EXTENSION_INCOMPATIBLE_OLDER);
                    });
                });

                it("should show enabled install button if latest requires newer API version", function () { // isCompatible: true, isLatestVersion: false, requiresNewer: true
                    runs(function () {
                        mockRegistry = { "mock-extension": makeMockExtension([">0.1", ">100.0"]) };
                        setupViewWithMockData(ExtensionManagerViewModel.RegistryViewModel);
                    });
                    runs(function () {
                        var $button = $("button.install[data-extension-id=mock-extension]", view.$el);
                        expect($button.length).toBe(1);
                        expect($button.prop("disabled")).toBeFalsy();
                        expect($("button.update[data-extension-id=mock-extension]", view.$el).length).toBe(0);

                        var $warning = $(".alert.warning", view.$el);
                        expect($warning.length).toBe(1);
                        expect($warning.text().trim()).toBe(StringUtils.format(Strings.EXTENSION_LATEST_INCOMPATIBLE_NEWER, "2.0.0", "1.0.0"));
                    });
                });

                it("should show enabled install button if latest requires older API version", function () { // isCompatible: true, isLatestVersion: false, requiresNewer: false
                    runs(function () {
                        mockRegistry = { "mock-extension": makeMockExtension([">0.1", "<0.2"]) };
                        setupViewWithMockData(ExtensionManagerViewModel.RegistryViewModel);
                    });
                    runs(function () {
                        var $button = $("button.install[data-extension-id=mock-extension]", view.$el);
                        expect($button.length).toBe(1);
                        expect($button.prop("disabled")).toBeFalsy();
                        expect($("button.update[data-extension-id=mock-extension]", view.$el).length).toBe(0);

                        var $warning = $(".alert.warning", view.$el);
                        expect($warning.length).toBe(1);
                        expect($warning.text().trim()).toBe(StringUtils.format(Strings.EXTENSION_LATEST_INCOMPATIBLE_OLDER, "2.0.0", "1.0.0"));
                    });
                });

                // 'Install' button action
                it("should bring up the install dialog and install an item when install button is clicked", function () {
                    runs(function () {
                        setupViewWithMockData(ExtensionManagerViewModel.RegistryViewModel);
                    });
                    runs(function () {
                        var $button = $("button.install[data-extension-id=mock-extension-3]", view.$el);
                        expect($button.length).toBe(1);
                        $button.click();
                        expect(InstallExtensionDialog.installUsingDialog)
                            .toHaveBeenCalledWith("http://fake-repository.com/mock-extension-3/mock-extension-3-1.0.0.zip");
                    });
                });

                it("should install latest compatible version", function () {
                    runs(function () {
                        mockRegistry = { "mock-extension": makeMockExtension([">0.1", ">0.2", ">100.0"]) };
                        setupViewWithMockData(ExtensionManagerViewModel.RegistryViewModel);
                    });
                    runs(function () {
                        var $button = $("button.install[data-extension-id=mock-extension]", view.$el);
                        expect($button.length).toBe(1);
                        $button.click();
                        expect(InstallExtensionDialog.installUsingDialog)
                            .toHaveBeenCalledWith("http://fake-repository.com/mock-extension/mock-extension-2.0.0.zip");
                    });
                });

                it("should disable the install button for an item immediately after installing it", function () {
                    var $button;
                    runs(function () {
                        setupViewWithMockData(ExtensionManagerViewModel.RegistryViewModel);
                    });
                    runs(function () {
                        $button = $("button.install[data-extension-id=mock-extension-3]", view.$el);
                        $button.click();
                    });
                    runs(function () {
                        // Have to get the button again since the view may have created a new button when re-rendering.
                        $button = $("button.install[data-extension-id=mock-extension-3]", view.$el);
                        expect($button.prop("disabled")).toBeTruthy();
                    });

                });

                // 'Update' button state (see also similar tests for InstalledViewModel below)
                it("should show enabled update button for items that have a compatible update available", function () {
                    mockRegistry = { "mock-extension": makeMockExtension([">0.1", ">0.1"]) };
                    var mockInstallInfo = { "mock-extension": { installInfo: makeMockInstalledVersion(mockRegistry["mock-extension"], "1.0.0") } };
                    ExtensionManager._setExtensions(mockInstallInfo);
                    setupViewWithMockData(ExtensionManagerViewModel.RegistryViewModel);

                    runs(function () {
                        var $button = $("button.update[data-extension-id=mock-extension]", view.$el);
                        expect($button.length).toBe(1);
                        expect($button.prop("disabled")).toBeFalsy();

                        expect($("button.install[data-extension-id=mock-extension]", view.$el).length).toBe(0);
                        expect($(".alert.warning", view.$el).length).toBe(0);
                    });
                });

                it("should show disabled update button for items whose available update requires newer API version", function () {   // isLatestVersion: false, requiresNewer: true
                    mockRegistry = { "mock-extension": makeMockExtension([">0.1", ">100.0"]) };
                    var mockInstallInfo = { "mock-extension": { installInfo: makeMockInstalledVersion(mockRegistry["mock-extension"], "1.0.0") } };
                    ExtensionManager._setExtensions(mockInstallInfo);
                    setupViewWithMockData(ExtensionManagerViewModel.RegistryViewModel);

                    runs(function () {
                        var $button = $("button.update[data-extension-id=mock-extension]", view.$el);
                        expect($button.length).toBe(1);
                        expect($button.prop("disabled")).toBeTruthy();

                        expect($("button.install[data-extension-id=mock-extension]", view.$el).length).toBe(0);
                        expect($(".alert.warning", view.$el).length).toBe(0);
                    });
                });

                it("should show disabled update button for items whose available update requires older API version", function () {   // isLatestVersion: false, requiresNewer: false
                    mockRegistry = { "mock-extension": makeMockExtension([">0.1", "<0.2"]) };
                    var mockInstallInfo = { "mock-extension": { installInfo: makeMockInstalledVersion(mockRegistry["mock-extension"], "1.0.0") } };
                    ExtensionManager._setExtensions(mockInstallInfo);
                    setupViewWithMockData(ExtensionManagerViewModel.RegistryViewModel);

                    runs(function () {
                        var $button = $("button.update[data-extension-id=mock-extension]", view.$el);
                        expect($button.length).toBe(1);
                        expect($button.prop("disabled")).toBeTruthy();

                        expect($("button.install[data-extension-id=mock-extension]", view.$el).length).toBe(0);
                        expect($(".alert.warning", view.$el).length).toBe(0);
                    });
                });

                it("should show disabled update button for items that are in dev folder and have a compatible update available", function () {
                    mockRegistry = { "mock-extension": makeMockExtension([">0.1", ">0.1"]) };
                    var mockInfo = makeMockInstalledVersion(mockRegistry["mock-extension"], "1.0.0");
                    mockInfo.locationType = ExtensionManager.LOCATION_DEV;
                    ExtensionManager._setExtensions({
                        "mock-extension": {
                            installInfo: mockInfo
                        }
                    });
                    setupViewWithMockData(ExtensionManagerViewModel.RegistryViewModel);

                    runs(function () {
                        var $button = $("button.update[data-extension-id=mock-extension]", view.$el);
                        expect($button.length).toBe(1);
                        expect($button.prop("disabled")).toBeTruthy();
                    });
                });

                // Info links action
                it("should open links in the native browser instead of in Brackets", function () {
                    runs(function () {
                        mockRegistry = {
                            "basic-valid-extension": {
                                "metadata": {
                                    "name": "basic-valid-extension",
                                    "title": "Basic Valid Extension",
                                    "version": "1.0.0"
                                },
                                "owner": "github:someuser",
                                "versions": [
                                    {
                                        "version": "1.0.0",
                                        "published": "2013-04-10T18:28:20.530Z"
                                    }
                                ]
                            }
                        };
                        setupViewWithMockData(ExtensionManagerViewModel.RegistryViewModel);
                    });
                    runs(function () {
                        var origHref = window.location.href;
                        spyOn(NativeApp, "openURLInDefaultBrowser");

                        var event = new window.Event("click", { bubbles: false, cancelable: true });
                        window.document.querySelector("a[href='https://github.com/someuser']").dispatchEvent(event);

                        expect(NativeApp.openURLInDefaultBrowser).toHaveBeenCalledWith("https://github.com/someuser");
                        expect(window.location.href).toBe(origHref);
                    });
                });
            });

            describe("when showing installed extensions", function () {

                it("should show the 'no extensions' message when there are no extensions installed", function () {
                    setupViewWithMockData(ExtensionManagerViewModel.InstalledViewModel);
                    runs(function () {
                        expect($(".empty-message", view.$el).css("display")).not.toBe("none");
                        expect($(".empty-message", view.$el).html()).toEqual(Strings.NO_EXTENSIONS);
                        expect($("table", view.$el).css("display")).toBe("none");
                    });
                });

                it("should show the 'no extensions match' message when there are extensions installed but none match the search query", function () {
                    mockLoadExtensions(["user/mock-extension-3", "user/mock-extension-4", "user/mock-legacy-extension"]);
                    setupViewWithMockData(ExtensionManagerViewModel.InstalledViewModel);
                    runs(function () {
                        view.filter("DON'T_FIND_ME_IN_THE_EXTENSION_LIST");
                        expect($(".empty-message", view.$el).css("display")).not.toBe("none");
                        expect($(".empty-message", view.$el).html()).toEqual(Strings.NO_EXTENSION_MATCHES);
                        expect($("table", view.$el).css("display")).toBe("none");
                    });
                });

                it("should show only items that are already installed and have remove and disable/enable buttons for each", function () {
                    mockLoadExtensions(["user/mock-extension-3", "user/mock-extension-4", "user/mock-legacy-extension"]);
                    mockLoadExtensions(["user/mock-extension-5"], "disabled");
                    setupViewWithMockData(ExtensionManagerViewModel.InstalledViewModel);
                    runs(function () {
                        expect($(".empty-message", view.$el).css("display")).toBe("none");
                        expect($("table", view.$el).css("display")).not.toBe("none");
                        _.forEach(mockRegistry, function (item) {
                            var $removeButton = $("button.remove[data-extension-id=" + item.metadata.name + "]", view.$el);
                            if (item.metadata.name === "mock-extension-3" ||
                                    item.metadata.name === "mock-extension-4" ||
                                    item.metadata.name === "mock-legacy-extension" ||
                                    item.metadata.name === "mock-extension-5") {
                                expect(view).toHaveText(item.metadata.name);
                                expect($removeButton.length).toBe(1);

                                // should also have disable/enable button
                                var isDisable = item.metadata.name === "mock-extension-5" ? false : true,
                                    $disableButton = $("button." + (isDisable ? "disable" : "enable") + "[data-extension-id=" +
                                                       item.metadata.name + "]", view.$el);

                                expect($disableButton.length).toBe(1);

                                // But no update button
                                var $updateButton = $("button.update[data-extension-id=" + item.metadata.name + "]", view.$el);
                                expect($updateButton.length).toBe(0);
                            } else {
                                expect(view).not.toHaveText(item.metadata.name);
                                expect($removeButton.length).toBe(0);
                            }
                        });

                        // And no overall update icon overlay
                        expect(model.notifyCount).toBe(0);
                    });
                });

                it("should show a newly installed extension", function () {
                    setupViewWithMockData(ExtensionManagerViewModel.InstalledViewModel);
                    runs(function () {
                        expect(view).not.toHaveText("mock-extension-3");
                        mockLoadExtensions(["user/mock-extension-3"]);
                    });
                    runs(function () {
                        expect(view).toHaveText("mock-extension-3");
                    });
                });

                it("should not show extensions in the default folder", function () {
                    mockLoadExtensions(["default/mock-extension-1"]);
                    setupViewWithMockData(ExtensionManagerViewModel.InstalledViewModel);
                    runs(function () {
                        expect(view).not.toHaveText("mock-extension-1");
                    });
                });

                // 'Remove' button state
                it("should show extensions that failed to load with a 'remove' link", function () {
                    mockLoadExtensions(["user/mock-extension-3"], true);
                    setupViewWithMockData(ExtensionManagerViewModel.InstalledViewModel);
                    runs(function () {
                        expect(view).toHaveText("mock-extension-3");
                        var $removeLink = $("a.remove[data-extension-id=mock-extension-3]", view.$el);
                        expect($removeLink.length).toBe(1);
                        expect($removeLink.prop("disabled")).toBeFalsy();

                        $removeLink.click();
                        expect(ExtensionManager.isMarkedForRemoval("mock-extension-3")).toBe(true);
                        var $undoLink = $("a.undo-remove[data-extension-id=mock-extension-3]", view.$el);
                        expect($undoLink.length).toBe(1);
                        $removeLink = $("a.remove[data-extension-id=mock-extension-3]", view.$el);
                        expect($removeLink.length).toBe(0);
                    });
                });

                it("should not have a 'remove' link for extensions in the dev folder that failed to load", function () {
                    mockLoadExtensions(["dev/mock-failed-in-dev-folder"], true);
                    setupViewWithMockData(ExtensionManagerViewModel.InstalledViewModel);
                    runs(function () {
                        expect(view).toHaveText("mock-failed-in-dev-folder");
                        var $removeLink = $("a.remove[data-extension-id=mock-failed-in-dev-folder]", view.$el);
                        expect($removeLink.length).toBe(0);
                    });
                });

                it("should disable the Remove button for extensions in the dev folder", function () {
                    mockLoadExtensions(["dev/mock-extension-2"]);
                    setupViewWithMockData(ExtensionManagerViewModel.InstalledViewModel);
                    runs(function () {
                        var $button = $("button.remove[data-extension-id=mock-extension-2]", view.$el);
                        expect($button.length).toBe(1);
                        expect($button.prop("disabled")).toBeTruthy();
                    });
                });

                // Disable button action
                it("should mark the given extension for disabling, hide the buttons and show the undo link", function () {
                    mockLoadExtensions(["user/mock-extension-3"]);
                    setupViewWithMockData(ExtensionManagerViewModel.InstalledViewModel);
                    runs(function () {
                        var $disableButton = $("button.disable[data-extension-id=mock-extension-3]", view.$el),
                            $removeButton,
                            $undoLink;
                        $disableButton.click();
                        $removeButton = $("button.remove[data-extension-id=mock-extension-3]", view.$el);
                        $undoLink = $("a.undo-disable[data-extension-id=mock-extension-3]", view.$el);
                        $disableButton = $("button.disable[data-extension-id=mock-extension-3]", view.$el);
                        expect($removeButton.length).toBe(0);
                        expect($undoLink.length).toBe(1);
                        expect($disableButton.length).toBe(0);
                        expect(ExtensionManager.isMarkedForDisabling("mock-extension-3")).toBe(true);
                    });
                });

                it("should undo mark for disabling and make the buttons available again", function () {
                    mockLoadExtensions(["user/mock-extension-3"]);
                    setupViewWithMockData(ExtensionManagerViewModel.InstalledViewModel);
                    runs(function () {
                        var $disableButton = $("button.disable[data-extension-id=mock-extension-3]", view.$el),
                            $removeButton,
                            $undoLink;
                        $disableButton.click();
                        $undoLink = $("a.undo-disable[data-extension-id=mock-extension-3]", view.$el);
                        $undoLink.click();
                        $removeButton = $("button.remove[data-extension-id=mock-extension-3]", view.$el);
                        $disableButton = $("button.disable[data-extension-id=mock-extension-3]", view.$el);
                        $undoLink = $("a.undo-disable[data-extension-id=mock-extension-3]", view.$el);
                        expect($removeButton.length).toBe(1);
                        expect($undoLink.length).toBe(0);
                        expect($disableButton.length).toBe(1);
                        expect(ExtensionManager.isMarkedForDisabling("mock-extension-3")).toBe(false);
                    });
                });

                it("should be able to disable an extension from the dev folder", function () {
                    mockLoadExtensions(["dev/mock-extension-6"]);
                    setupViewWithMockData(ExtensionManagerViewModel.InstalledViewModel);
                    runs(function () {
                        var $disableButton = $("button.disable[data-extension-id=mock-extension-6]", view.$el);
                        expect($disableButton.prop("disabled")).toBeFalsy();
                        $disableButton.click();
                        expect(ExtensionManager.isMarkedForDisabling("mock-extension-6")).toBe(true);
                    });
                });

                // 'Remove' button action
                it("should mark the given extension for removal, hide the remove button, and show an undo link", function () {
                    mockLoadExtensions(["user/mock-extension-3"]);
                    setupViewWithMockData(ExtensionManagerViewModel.InstalledViewModel);
                    runs(function () {
                        var $removeButton = $("button.remove[data-extension-id=mock-extension-3]", view.$el),
                            $disableButton;
                        $removeButton.click();
                        $disableButton = $("button.disable[data-extension-id=mock-extension-3]", view.$el);
                        expect(ExtensionManager.isMarkedForRemoval("mock-extension-3")).toBe(true);
                        expect($disableButton.length).toBe(0);
                        var $undoLink = $("a.undo-remove[data-extension-id=mock-extension-3]", view.$el);
                        expect($undoLink.length).toBe(1);
                        $removeButton = $("button.remove[data-extension-id=mock-extension-3]", view.$el);
                        expect($removeButton.length).toBe(0);
                    });
                });

                it("should undo marking an extension for removal", function () {
                    mockLoadExtensions(["user/mock-extension-3"]);
                    setupViewWithMockData(ExtensionManagerViewModel.InstalledViewModel);
                    runs(function () {
                        var $button = $("button.remove[data-extension-id=mock-extension-3]", view.$el);
                        $button.click();
                        var $undoLink = $("a.undo-remove[data-extension-id=mock-extension-3]", view.$el);
                        $undoLink.click();
                        expect(ExtensionManager.isMarkedForRemoval("mock-extension-3")).toBe(false);
                        $button = $("button.remove[data-extension-id=mock-extension-3]", view.$el);
                        expect($button.length).toBe(1);
                    });
                });

                it("should mark a legacy extension for removal", function () {
                    var id = "mock-legacy-extension";
                    mockLoadExtensions(["user/" + id]);
                    setupViewWithMockData(ExtensionManagerViewModel.InstalledViewModel);
                    runs(function () {
                        var $button = $("button.remove[data-extension-id='" + id + "']", view.$el);
                        $button.click();
                        expect(ExtensionManager.isMarkedForRemoval(id)).toBe(true);
                    });
                });

                it("should no longer show a fully removed extension", function () {
                    mockLoadExtensions(["user/mock-extension-3", "user/mock-extension-4", "user/mock-legacy-extension"]);
                    setupViewWithMockData(ExtensionManagerViewModel.InstalledViewModel);
                    runs(function () {
                        expect(view).toHaveText("mock-extension-3");
                        waitsForDone(ExtensionManager.remove("mock-extension-3"));
                    });
                    runs(function () {
                        expect(view).not.toHaveText("mock-extension-3");
                    });
                });

                // 'Update' button state (see also similar tests for RegistryViewModel above)
                it("should show enabled update button for items that have a compatible update available", function () {
                    mockRegistry = { "mock-extension": makeMockExtension([">0.1", ">0.1"]) };
                    var mockInstallInfo = { "mock-extension": { installInfo: makeMockInstalledVersion(mockRegistry["mock-extension"], "1.0.0") } };
                    ExtensionManager._setExtensions(mockInstallInfo);
                    waitsForDone(ExtensionManager.downloadRegistry()); // ensure mockRegistry integrated in
                    runs(function () {
                        setupViewWithMockData(ExtensionManagerViewModel.InstalledViewModel);
                    });
                    runs(function () {
                        var $button = $("button.update[data-extension-id=mock-extension]", view.$el);
                        expect($button.length).toBe(1);
                        expect($button.prop("disabled")).toBeFalsy();
                        expect(model.notifyCount).toBe(1);
                        expect($("button.install[data-extension-id=mock-extension]", view.$el).length).toBe(0);
                    });
                });

                it("should show disabled update button for items whose available update requires newer API version", function () {   // isLatestVersion: false, requiresNewer: true
                    mockRegistry = { "mock-extension": makeMockExtension([">0.1", ">100.0"]) };
                    var mockInstallInfo = { "mock-extension": { installInfo: makeMockInstalledVersion(mockRegistry["mock-extension"], "1.0.0") } };
                    ExtensionManager._setExtensions(mockInstallInfo);
                    waitsForDone(ExtensionManager.downloadRegistry()); // ensure mockRegistry integrated in
                    runs(function () {
                        setupViewWithMockData(ExtensionManagerViewModel.InstalledViewModel);
                    });
                    runs(function () {
                        var $button = $("button.update[data-extension-id=mock-extension]", view.$el);
                        expect($button.length).toBe(1);
                        expect($button.prop("disabled")).toBeTruthy();

                        // notify count doesn't show extensions that cannot be updated
                        expect(model.notifyCount).toBe(0);

                        expect($("button.install[data-extension-id=mock-extension]", view.$el).length).toBe(0);
                        expect($(".alert.warning", view.$el).length).toBe(0);
                    });
                });
                it("should show disabled update button for items whose available update requires older API version", function () {   // isLatestVersion: false, requiresNewer: false
                    mockRegistry = { "mock-extension": makeMockExtension([">0.1", "<0.2"]) };
                    var mockInstallInfo = { "mock-extension": { installInfo: makeMockInstalledVersion(mockRegistry["mock-extension"], "1.0.0") } };
                    ExtensionManager._setExtensions(mockInstallInfo);
                    waitsForDone(ExtensionManager.downloadRegistry()); // ensure mockRegistry integrated in
                    runs(function () {
                        setupViewWithMockData(ExtensionManagerViewModel.InstalledViewModel);
                    });
                    runs(function () {
                        var $button = $("button.update[data-extension-id=mock-extension]", view.$el);
                        expect($button.length).toBe(1);
                        expect($button.prop("disabled")).toBeTruthy();

                        // notify count doesn't show extensions that cannot be updated
                        expect(model.notifyCount).toBe(0);

                        expect($("button.install[data-extension-id=mock-extension]", view.$el).length).toBe(0);
                        expect($(".alert.warning", view.$el).length).toBe(0);
                    });
                });

                // 'Update' button action
                it("should mark the given extension for update, hide the remove button, and show an undo link", function () {
                    var id = "mock-extension-3";
                    mockLoadExtensions(["user/" + id]);
                    setupViewWithMockData(ExtensionManagerViewModel.InstalledViewModel);
                    runs(function () {
                        ExtensionManager.updateFromDownload({
                            name: id,
                            installationStatus: "NEEDS_UPDATE"
                        });
                        expect(ExtensionManager.isMarkedForUpdate(id)).toBe(true);
                        var $undoLink = $("a.undo-update[data-extension-id=" + id + "]", view.$el);
                        expect($undoLink.length).toBe(1);
                        var $button = $("button.remove[data-extension-id=" + id + "]", view.$el);
                        expect($button.length).toBe(0);
                    });
                });

                it("should undo marking an extension for update", function () {
                    var id = "mock-extension-3",
                        filename = "/path/to/downloaded/file.zip",
                        file = FileSystem.getFileForPath(filename);
                    mockLoadExtensions(["user/" + id]);
                    setupViewWithMockData(ExtensionManagerViewModel.InstalledViewModel);
                    runs(function () {
                        ExtensionManager.updateFromDownload({
                            name: id,
                            installationStatus: "NEEDS_UPDATE",
                            localPath: filename
                        });
                        spyOn(file, "unlink");
                        var $undoLink = $("a.undo-update[data-extension-id=" + id + "]", view.$el);
                        $undoLink.click();
                        expect(ExtensionManager.isMarkedForUpdate(id)).toBe(false);
                        expect(file.unlink).toHaveBeenCalled();
                        var $button = $("button.remove[data-extension-id=" + id + "]", view.$el);
                        expect($button.length).toBe(1);
                    });
                });

                it("should properly return information about available updates and clean it after updates are installed", function () {
                    mockRegistry = { "mock-extension": makeMockExtension([">0.1", ">0.1"]) };
                    var mockInstallInfo = { "mock-extension": { installInfo: makeMockInstalledVersion(mockRegistry["mock-extension"], "1.0.0") } };
                    ExtensionManager._setExtensions(mockInstallInfo);
                    waitsForDone(ExtensionManager.downloadRegistry()); // ensure mockRegistry integrated in
                    runs(function () {
                        setupViewWithMockData(ExtensionManagerViewModel.InstalledViewModel);
                    });
                    runs(function () {
                        var availableUpdates = ExtensionManager.getAvailableUpdates();
                        expect(availableUpdates.length).toBe(1);
                        // cleanAvailableUpdates shouldn't clean the array
                        expect(ExtensionManager.cleanAvailableUpdates(availableUpdates).length).toBe(1);
                        // now simulate that update is installed and see if cleanAvailableUpdates() method will work
                        ExtensionManager.extensions["mock-extension"].installInfo.metadata.version =
                            ExtensionManager.extensions["mock-extension"].registryInfo.metadata.version;
                        expect(ExtensionManager.cleanAvailableUpdates(availableUpdates).length).toBe(0);
                    });
                });

                // i18n info
                it("should show correct i18n info if the extension is translated in user's lang", function () {
                    var mockInstallInfo = { "mock-extension-4": { installInfo: mockRegistry["mock-extension-4"] } };
                    mockInstallInfo["mock-extension-4"].installInfo.metadata.i18n = ["zh-cn", "foo", "en"];
                    ExtensionManager._setExtensions(mockInstallInfo);
                    setupViewWithMockData(ExtensionManagerViewModel.InstalledViewModel);
                    runs(function () {
                        var $extTranslated  = $(".ext-translated", view.$el),
                            languages       = [LocalizationUtils.getLocalizedLabel("en"), "foo",  LocalizationUtils.getLocalizedLabel("zh-cn")];
                        expect($extTranslated.length).toBe(1);
                        expect($extTranslated.text()).toBe(StringUtils.format(Strings.EXTENSION_TRANSLATED_USER_LANG, languages.length));
                        expect($extTranslated.attr("title")).toBe(StringUtils.format(Strings.EXTENSION_TRANSLATED_LANGS, languages.join(", ")));
                    });
                });
                it("should show correct i18n info if the extension is translated in some other languages", function () {
                    var mockInstallInfo = { "mock-extension-4": { installInfo: mockRegistry["mock-extension-4"] } };
                    mockInstallInfo["mock-extension-4"].installInfo.metadata.i18n = ["zh-cn"];
                    ExtensionManager._setExtensions(mockInstallInfo);
                    setupViewWithMockData(ExtensionManagerViewModel.InstalledViewModel);
                    runs(function () {
                        var $extTranslated  = $(".ext-translated", view.$el),
                            languages       = [LocalizationUtils.getLocalizedLabel("zh-cn")];
                        expect($extTranslated.length).toBe(1);
                        expect($extTranslated.text()).toBe(StringUtils.format(Strings.EXTENSION_TRANSLATED_GENERAL, languages.length));
                        expect($extTranslated.attr("title")).toBe(StringUtils.format(Strings.EXTENSION_TRANSLATED_LANGS, languages.join(", ")));
                    });
                });
                it("should not show i18n info if the extension isn't translated", function () {
                    var mockInstallInfo = { "mock-extension-4": { installInfo: mockRegistry["mock-extension-4"] } };
                    mockInstallInfo["mock-extension-4"].installInfo.metadata.i18n = [];
                    ExtensionManager._setExtensions(mockInstallInfo);
                    setupViewWithMockData(ExtensionManagerViewModel.InstalledViewModel);
                    runs(function () {
                        var $extTranslated  = $(".ext-translated", view.$el);
                        expect($extTranslated.length).toBe(0);
                        expect($extTranslated.attr("title")).toBe(undefined);
                    });
                });

            });


            describe("ExtensionManagerDialog", function () {
                var dialogClassShown, didReload, didClose, $mockDlg;

                describe("_performChanges", function () {

                    beforeEach(function () {
                        // Mock popping up dialogs
                        dialogClassShown = null;
                        didClose = false;
                        spyOn(Dialogs, "showModalDialog").andCallFake(function (dlgClass, title, message) {
                            dialogClassShown = dlgClass;
                            $mockDlg = $("<div/>").addClass(dlgClass);
                            // The test will resolve the promise.
                            return {
                                getElement: function () { return $mockDlg; },
                                close: function () { didClose = true; }
                            };
                        });

                        // Mock reloading the app so we don't actually reload :)
                        didReload = false;
                        spyOn(CommandManager, "execute").andCallFake(function (id) {
                            if (id === Commands.APP_RELOAD) {
                                didReload = true;
                            } else {
                                CommandManager.execute.apply(this, arguments);
                            }
                        });
                    });

                    afterEach(function () {
                        ExtensionManager._reset();
                    });

                    it("should not show a removal confirmation dialog if no extensions were removed", function () {
                        mockLoadExtensions(["user/mock-extension-3"]);
                        runs(function () {
                            ExtensionManagerDialog._performChanges();
                            expect(dialogClassShown).toBeFalsy();
                        });
                    });

                    it("should not show a removal confirmation dialog if an extension was marked for removal and then unmarked", function () {
                        mockLoadExtensions(["user/mock-extension-3"]);
                        setupViewWithMockData(ExtensionManagerViewModel.InstalledViewModel);
                        runs(function () {
                            var $button = $("button.remove[data-extension-id=mock-extension-3]", view.$el);
                            $button.click();
                            var $undoLink = $("a.undo-remove[data-extension-id=mock-extension-3]", view.$el);
                            $undoLink.click();
                            ExtensionManagerDialog._performChanges();
                            expect(dialogClassShown).toBeFalsy();
                        });
                    });

                    it("should show a removal confirmation dialog if an extension was removed", function () {
                        mockLoadExtensions(["user/mock-extension-3"]);
                        setupViewWithMockData(ExtensionManagerViewModel.InstalledViewModel);
                        runs(function () {
                            var $button = $("button.remove[data-extension-id=mock-extension-3]", view.$el);
                            $button.click();
                        });
                        runs(function () {
                            // Don't expect the model to be disposed until after the dialog is dismissed.
                            ExtensionManagerDialog._performChanges();
                            expect(dialogClassShown).toBe("change-marked-extensions");
                            $mockDlg.triggerHandler("buttonClick", Dialogs.DIALOG_BTN_CANCEL);
                        });
                    });

                    it("should remove extensions and quit if the user hits Remove and Quit on the removal confirmation dialog", function () {
                        mockLoadExtensions(["user/mock-extension-3"]);
                        setupViewWithMockData(ExtensionManagerViewModel.InstalledViewModel);
                        runs(function () {
                            var $button = $("button.remove[data-extension-id=mock-extension-3]", view.$el);
                            $button.click();
                        });
                        runs(function () {
                            // Don't expect the model to be disposed until after the dialog is dismissed.
                            ExtensionManagerDialog._performChanges();
                            $mockDlg.triggerHandler("buttonClick", Dialogs.DIALOG_BTN_OK);
                        });
                        waitsFor(function () { return didReload; }, "mock reload");
                        runs(function () {
                            var mockPath = SpecRunnerUtils.getTestPath("/spec/ExtensionManager-test-files");
                            expect(removedPath).toBe(mockPath + "/user/mock-extension-3");
                            expect(didClose).toBe(true);
                            expect(didReload).toBe(true);
                        });
                    });

                    it("should not remove extensions or quit if the user hits Cancel on the removal confirmation dialog", function () {
                        mockLoadExtensions(["user/mock-extension-3"]);
                        setupViewWithMockData(ExtensionManagerViewModel.InstalledViewModel);
                        runs(function () {
                            var $button = $("button.remove[data-extension-id=mock-extension-3]", view.$el);
                            $button.click();
                        });
                        runs(function () {
                            // Don't expect the model to be disposed until after the dialog is dismissed.
                            ExtensionManagerDialog._performChanges();
                            $mockDlg.triggerHandler("buttonClick", Dialogs.DIALOG_BTN_CANCEL);
                            expect(removedPath).toBeFalsy();
                            expect(ExtensionManager.isMarkedForRemoval("mock-extension-3")).toBe(false);
                            expect(didClose).toBe(true);
                            expect(didReload).toBe(false);
                        });
                    });

                    it("should not show a disabling confirmation dialog if an extension was marked and then unmarked", function () {
                        mockLoadExtensions(["user/mock-extension-3"]);
                        setupViewWithMockData(ExtensionManagerViewModel.InstalledViewModel);
                        runs(function () {
                            var $button = $("button.disable[data-extension-id=mock-extension-3]", view.$el),
                                $undoLink;
                            $button.click();
                            $undoLink = $("a.undo-disable[data-extension-id=mock-extension-3]");
                            $undoLink.click();
                            ExtensionManagerDialog._performChanges();
                            expect(dialogClassShown).toBeFalsy();
                        });
                    });

                    it("should show a disabling confirmation dialog if an extension was marked for disabling", function () {
                        mockLoadExtensions(["user/mock-extension-3"]);
                        setupViewWithMockData(ExtensionManagerViewModel.InstalledViewModel);
                        runs(function () {
                            var $button = $("button.disable[data-extension-id=mock-extension-3]", view.$el);
                            $button.click();
                        });
                        runs(function () {
                            ExtensionManagerDialog._performChanges();
                            expect(dialogClassShown).toBe("change-marked-extensions");
                            $mockDlg.triggerHandler("buttonClick", Dialogs.DIALOG_BTN_CANCEL);
                        });
                    });

                    it("should update extensions and quit if the user hits Update and Quit on the removal confirmation dialog", function () {
                        var id = "mock-extension-3",
                            filename = "/path/to/downloaded/mock-extension-3.zip";
                        mockLoadExtensions(["user/" + id]);
                        setupViewWithMockData(ExtensionManagerViewModel.InstalledViewModel);
                        var installDeferred = $.Deferred();
                        spyOn(Package, "installUpdate").andReturn(installDeferred.promise());
                        runs(function () {
                            ExtensionManager.updateFromDownload({
                                installationStatus: Package.InstallationStatuses.NEEDS_UPDATE,
                                localPath: filename,
                                name: id
                            });
                            // Don't expect the model to be disposed until after the dialog is dismissed.
                            ExtensionManagerDialog._performChanges();
                            $mockDlg.triggerHandler("buttonClick", Dialogs.DIALOG_BTN_OK);
                            installDeferred.resolve({
                                installationStatus: "INSTALLED"
                            });
                        });
                        waitsFor(function () { return didReload; }, "mock reload");
                        runs(function () {
                            expect(Package.installUpdate).toHaveBeenCalledWith(filename, id);
                            expect(didClose).toBe(true);
                            expect(didReload).toBe(true);
                        });
                    });

                    it("should not update extensions or quit if the user hits Cancel on the confirmation dialog", function () {
                        var id = "mock-extension-3",
                            filename = "/path/to/downloaded/file.zip",
                            file = FileSystem.getFileForPath(filename);
                        mockLoadExtensions(["user/" + id]);
                        setupViewWithMockData(ExtensionManagerViewModel.InstalledViewModel);
                        runs(function () {
                            ExtensionManager.updateFromDownload({
                                name: id,
                                localPath: filename,
                                installationStatus: Package.InstallationStatuses.NEEDS_UPDATE
                            });
                            expect(ExtensionManager.isMarkedForUpdate(id)).toBe(true);
                            spyOn(file, "unlink");
                            // Don't expect the model to be disposed until after the dialog is dismissed.
                            ExtensionManagerDialog._performChanges();
                            $mockDlg.triggerHandler("buttonClick", Dialogs.DIALOG_BTN_CANCEL);
                            expect(removedPath).toBeFalsy();
                            expect(ExtensionManager.isMarkedForUpdate("mock-extension-3")).toBe(false);
                            expect(didClose).toBe(true);
                            expect(didReload).toBe(false);
                            expect(file.unlink).toHaveBeenCalled();
                        });
                    });

                });

                describe("initialization", function () {

                    var dialog, $dlg, originalRegistry;

                    // Sets up a view without actually loading any data--just for testing how we
                    // respond to the notifications.
                    beforeEach(function () {
                        runs(function () {
                            fakeLoadDeferred = new $.Deferred();
                            spyOn(ExtensionManager, "downloadRegistry").andCallFake(function () {
                                return fakeLoadDeferred.promise();
                            });
                        });
                    });

                    afterEach(function () {
                        runs(function () {
                            dialog.close();
                            waitsForDone(dialog.getPromise(), "ExtensionManagerDialog.close");
                        });

                        runs(function () {
                            brackets.config.extension_registry = originalRegistry;
                            dialog = null;
                            $dlg = null;
                        });
                    });

                    function openDialog() {
                        // this command is synchronous
                        CommandManager.execute(Commands.FILE_EXTENSION_MANAGER)
                            .done(function (dialogResult) {
                                dialog = dialogResult;
                                $dlg = dialog.getElement();
                            });
                    }

                    function setRegistryURL(url) {
                        originalRegistry = brackets.config.extension_registry;
                        brackets.config.extension_registry = url;
                    }

                    it("should show the spinner before the registry appears successfully and hide it after", function () {
                        runs(function () {
                            openDialog();
                            expect($(".spinner", $dlg).length).toBe(1);
                            fakeLoadDeferred.resolve();
                            expect($(".spinner", $dlg).length).toBe(0);
                        });
                    });

                    it("should show an error and remove the spinner if there is an error fetching the registry", function () {
                        runs(function () {
                            openDialog();
                            fakeLoadDeferred.reject();
                            expect($(".spinner", $dlg).length).toBe(0);
                            expect($("#registry .empty-message").text()).toBe(Strings.EXTENSION_MANAGER_ERROR_LOAD);
                        });
                    });

                    it("should hide the registry tab when no URL is specified", function () {
                        runs(function () {
                            setRegistryURL(null);
                            openDialog();
                            fakeLoadDeferred.resolve();
                            expect($(".registry", $dlg).length).toBe(0);
                        });
                    });

                    it("should show the registry tab when a URL is specified", function () {
                        runs(function () {
                            setRegistryURL("not null");
                            openDialog();
                            fakeLoadDeferred.resolve();
                            expect($(".registry", $dlg).length).toBe(1);
                        });
                    });
                });
            });
        });

        describe("ExtensionManagerView-i18n", function () {

            beforeEach(function () {
                setupExtensionManagerViewTests(this);
                spyOn(brackets, "getLocale").andReturn("fr");
            });

            afterEach(function () {
                cleanupExtensionManagerViewTests();
            });

            it("should display localized description", function () {
                setupViewWithMockData(ExtensionManagerViewModel.RegistryViewModel);
                runs(function () {
                    _.forEach(mockRegistry, function (item) {
                        if (item.metadata["package-i18n"] &&
                                item.metadata["package-i18n"].hasOwnProperty("fr") &&
                                item.metadata["package-i18n"].fr.hasOwnProperty("description")) {
                            expect(view).toHaveText(item.metadata["package-i18n"].fr.description);
                            expect(view).toHaveText(item.metadata["package-i18n"].fr.title);
                            expect(view).not.toHaveText(item.metadata["package-i18n"].fr.warnings);
                        }
                    });
                });
            });
        });
    });
});
