define(function(require, exports, module) {

    // Load dependent modules
    var SpecRunnerUtils     = require("./SpecRunnerUtils.js")
    ;

    // These are tests for the low-level file io routines in brackets-app. Make sure
    // you have the latest brackets-app before running.

    describe("LowLevelFileIO", function() {

        it("should have a brackets.fs namespace", function() {
            expect(brackets.fs).toBeTruthy();
        });

        // Get window.location and remove the initial "file://" or "http://"
        var baseDir = SpecRunnerUtils.getTestPath("/spec/LowLevelFileIO-test-files/");

        beforeEach(function() {
            // Pre-test setup - set permissions on special directories

            // Set read-only mode
            brackets.fs.chmod(baseDir + "cant_read_here", 0222, function(err) {
                expect(err).toBeFalsy();
            });

            // Set write-only mode
            brackets.fs.chmod(baseDir + "cant_write_here", 0444, function(err) {
                expect(err).toBeFalsy();
            });
        });

        afterEach(function() {
            // Restore directory permissions

            // Set read-only mode
            brackets.fs.chmod(baseDir + "cant_read_here", 0777, function(err) {
                expect(err).toBeFalsy();
            });

            // Set write-only mode
            brackets.fs.chmod(baseDir + "cant_write_here", 0777, function(err) {
                expect(err).toBeFalsy();
            });
        });

        describe("readdir", function() {
            var complete, error, content;

            beforeEach(function() {
                complete = false;
            });

            it("should read a directory from disk", function() {
                brackets.fs.readdir(baseDir, function(err, contents) {
                    error = err;
                    content = contents;
                    complete = true;
                });

                waitsFor(function() { return complete }, 1000);

                runs(function() {
                    expect(error).toBeFalsy();

                    // Look for known files
                    expect(content.indexOf("file_one.txt")).not.toBe(-1);
                    expect(content.indexOf("file_two.txt")).not.toBe(-1);
                    expect(content.indexOf("file_three.txt")).not.toBe(-1);

                    // Make sure '.' and '..' are omitted
                    expect(content.indexOf(".")).toBe(-1);
                    expect(content.indexOf("..")).toBe(-1);
                });
            });

            it ("should return an error if the directory doesn't exist", function() {
                brackets.fs.readdir("/This/directory/doesnt/exist", function(err, contents) {
                    error = err;
                    complete = true;
                });

                waitsFor(function() { return complete; }, 1000);

                runs(function() {
                    expect(error).toBe(brackets.fs.ERR_NOT_FOUND);
                });
            });

            it ("should return an error if the directory can't be read", function() {
                brackets.fs.readdir(baseDir + "cant_read_here", function(err, contents) {
                    error = err;
                    complete = true;
                });

                waitsFor(function() { return complete; }, 1000);

                runs(function() {
                    expect(error).toBe(brackets.fs.ERR_CANT_READ);
                });
            });

            it ("should return an error if invalid parameters are passed", function() {
                brackets.fs.readdir(42, function(err, contents) {
                    error = err;
                    complete = true;
                });

                waitsFor(function() { return complete; }, 1000);

                runs(function() {
                    expect(error).toBe(brackets.fs.ERR_INVALID_PARAMS);
                });
            });
        }); // describe("readdir")

        describe("stat", function() {
            var complete, error, stat;

            beforeEach(function() {
                complete = false;
            });

            it ("should return correct information for a directory", function() {
                brackets.fs.stat(baseDir, function(err, _stat) {
                    error = err;
                    stat = _stat;
                    complete = true;
                });

                waitsFor(function() { return complete; }, 1000);

                runs(function() {
                    expect(error).toBeFalsy();
                    expect(stat.isDirectory()).toBe(true);
                    expect(stat.isFile()).toBe(false);
                });
            });

            it ("should return correct information for a file", function() {
                brackets.fs.stat(baseDir + "file_one.txt", function(err, _stat) {
                    error = err;
                    stat = _stat;
                    complete = true;
                });

                waitsFor(function() { return complete; }, 1000);

                runs(function() {
                    expect(error).toBeFalsy();
                    expect(stat.isDirectory()).toBe(false);
                    expect(stat.isFile()).toBe(true);
                });
            });

            it ("should return an error if the file/directory doesn't exist", function() {
                brackets.fs.stat("/This/directory/doesnt/exist", function(err, _stat) {
                    error = err;
                    complete = true;
                });

                waitsFor(function() { return complete; }, 1000);

                runs(function() {
                    expect(error).toBe(brackets.fs.ERR_NOT_FOUND);
                });
            });

            it ("should return an error if incorrect parameters are passed", function() {
                brackets.fs.stat(42, function(err, _stat) {
                    error = err;
                    complete = true;
                });

                waitsFor(function() { return complete; }, 1000);

                runs(function() {
                    expect(error).toBe(brackets.fs.ERR_INVALID_PARAMS);
                });
            });

        }); // describe("stat")

        describe("readFile", function() {
            var complete, error, content;

            beforeEach(function() {
                complete = false;
            });

            it ("should read a text file", function() {
                brackets.fs.readFile(baseDir + "file_one.txt", "utf8", function(err, contents) {
                    error = err;
                    content = contents;
                    complete = true;
                });

                waitsFor(function() { return complete; }, 1000);

                runs(function() {
                    expect(error).toBeFalsy();
                    expect(content).toBe("Hello world");
                });
            });

            it ("should return an error if trying to read a non-existent file", function() {
                brackets.fs.readFile("/This/file/doesnt/exist.txt", "utf8", function(err, contents) {
                    error = err;
                    content = contents;
                    complete = true;
                });

                waitsFor(function() { return complete; }, 1000);

                runs(function() {
                    expect(error).toBe(brackets.fs.ERR_NOT_FOUND);
                });
            });

            it ("should return an error if trying to use an unsppported encoding", function() {
                brackets.fs.readFile(baseDir + "file_one.txt", "utf16", function(err, contents) {
                    error = err;
                    content = contents;
                    complete = true;
                });

                waitsFor(function() { return complete; }, 1000);

                runs(function() {
                    expect(error).toBe(brackets.fs.ERR_UNSUPPORTED_ENCODING);
                });
            });

            it ("should return an error if called with invalid parameters", function() {
                brackets.fs.readFile(42, [], function(err, contents) {
                    error = err;
                    content = contents;
                    complete = true;
                });

                waitsFor(function() { return complete; }, 1000);

                runs(function() {
                    expect(error).toBe(brackets.fs.ERR_INVALID_PARAMS);
                });
            });

            it ("should return an error if trying to read a directory", function() {
                brackets.fs.readFile(baseDir, "utf8", function(err, contents) {
                    error = err;
                    content = contents;
                    complete = true;
                });

                waitsFor(function() { return complete; }, 1000);

                runs(function() {
                    expect(error).toBe(brackets.fs.ERR_CANT_READ);
                });
            })
        }); // describe("readFile")

        describe("writeFile", function() {
            var complete, error, content, contents = "This content was generated from LowLevelFileIO-test.js";

            beforeEach(function() {
                complete = false;
            });

            it ("should write the entire contents of a file", function() {
                brackets.fs.writeFile(baseDir + "write_test.txt", contents, "utf8", function(err) {
                    error = err;
                    complete = true;
                });

                waitsFor(function() { return complete; }, 1000);

                runs(function() {
                    expect(error).toBeFalsy();
                });

                // Read contents to verify
                runs(function() {
                    complete = false;
                    brackets.fs.readFile(baseDir + "write_test.txt", "utf8", function(err, data) {
                        error = err;
                        content = data;
                        complete = true;
                    });
                });

                waitsFor(function() { return complete; }, 1000);

                runs(function() {
                    expect(error).toBeFalsy();
                    expect(content).toBe(contents);
                });
            });

            it ("should return an error if the file can't be written", function() {
                brackets.fs.writeFile(baseDir + "cant_write_here/write_test.txt", contents, "utf8", function(err) {
                    error = err;
                    complete = true;
                });

                waitsFor(function() { return complete; }, 1000);

                runs(function() {
                    expect(error).toBe(brackets.fs.ERR_CANT_WRITE);
                });
            });

            it ("should return an error if called with invalid parameters", function() {
                brackets.fs.writeFile(42, contents, 2, function(err) {
                    error = err;
                    complete = true;
                });

                waitsFor(function() { return complete; }, 1000);

                runs(function() {
                    expect(error).toBe(brackets.fs.ERR_INVALID_PARAMS);
                });
            });

            it ("should return an error if trying to write a directory", function() {
                brackets.fs.writeFile(baseDir, contents, "utf8", function(err) {
                    error = err;
                    complete = true;
                });

                waitsFor(function() { return complete; }, 1000);

                runs(function() {
                    // Ideally we would get ERR_CANT_WRITE, but as long as we get some sort of error it's fine.
                    expect(error).toBeTruthy();
                });
            })
        }); // describe("writeFile")

        describe("unlink", function() {
            var complete, error, content, contents = "This content was generated from LowLevelFileIO-test.js";

            beforeEach(function() {
                complete = false;
            });

            it ("should remove a file", function() {
                var filename = baseDir + "remove_me.txt";

                brackets.fs.writeFile(filename, contents, "utf8", function(err) {
                    error = err;
                    complete = true;
                });

                waitsFor(function() { return complete; }, 1000);

                runs(function() {
                    expect(error).toBeFalsy();
                });

                // Read contents to verify
                runs(function() {
                    complete = false;
                    brackets.fs.readFile(filename, "utf8", function(err, data) {
                        error = err;
                        content = data;
                        complete = true;
                    });
                });

                waitsFor(function() { return complete; }, 1000);

                runs(function() {
                    expect(error).toBeFalsy();
                    expect(content).toBe(contents);
                });

                // Remove the file
                runs(function() {
                    complete = false;
                    brackets.fs.unlink(filename, function(err) {
                        error = err;
                        complete = true;
                    });
                });

                waitsFor(function() { return complete; }, 1000);

                runs(function() {
                    expect(error).toBeFalsy();
                });

                // Verify it is gone
                runs(function() {
                    complete = false;
                    brackets.fs.stat(filename, function(err, stat) {
                        error = err;
                        complete = true;
                    });
                });

                waitsFor(function() { return complete; }, 1000);

                runs(function() {
                    expect(error).toBe(brackets.fs.ERR_NOT_FOUND);
                });
            });

            it ("should return an error if the file doesn't exist", function() {
                brackets.fs.unlink("/This/file/doesnt/exist.txt", function(err) {
                    error = err;
                    complete = true;
                });

                waitsFor(function() { return complete; }, 1000);

                runs(function() {
                    expect(error).toBe(brackets.fs.ERR_NOT_FOUND);
                });
            });

            it ("should return an error if the a directory is specified", function() {
                brackets.fs.unlink(baseDir, function(err) {
                    error = err;
                    complete = true;
                });

                waitsFor(function() { return complete; }, 1000);

                runs(function() {
                    expect(error).toBe(brackets.fs.ERR_NOT_FILE);
                });
            });

            it ("should return an error if called with invalid parameters", function() {
                brackets.fs.unlink(42, function(err) {
                    error = err;
                    complete = true;
                });

                waitsFor(function() { return complete; }, 1000);

                runs(function() {
                    expect(error).toBe(brackets.fs.ERR_INVALID_PARAMS);
                });
            });

        }); // describe("unlink")
    });
});


// jstree dblclick handling seems to steal focus from editor, so set focus again

/*
 * Copyright 2011 Adobe Systems Incorporated. All Rights Reserved.
 */

/**
 * ProjectManager is the model for the set of currently open project. It is responsible for
 * creating and updating the project tree when projects are opened and when changes occur to
 * the file tree.
 *
 * This module dispatches 1 event:
 *    - isFirstProjectOpen -- When the ProjectManager initializes the first
 *                            project at application start-up.
 *
 * These are jQuery events, so to listen for them you do something like this:
 *    $(ProjectManager).on("eventname", handler);
 */
define(function(require, exports, module) {
    // Load dependent non-module scripts
    require("thirdparty/jstree_pre1.0_fix_1/jquery.jstree");

    // Load dependent modules
    var NativeFileSystem    = require("NativeFileSystem").NativeFileSystem
    ,   PreferencesManager  = require("PreferencesManager")
    ,   DocumentManager     = require("DocumentManager")
    ,   CommandManager      = require("CommandManager")
    ,   Commands            = require("Commands")
    ,   Strings             = require("strings")
    ;

    /**
     * Unique PreferencesManager clientID
     */
    var PREFERENCES_CLIENT_ID = "com.adobe.brackets.ProjectManager";

    /**
     * Returns the root folder of the currently loaded project, or null if no project is open (during
     * startup, or running outside of app shell).
     * @return {DirectoryEntry}
     */
    function getProjectRoot() {
        return _projectRoot;
    }
    /**
     * @private
     * @see getProjectRoot()
     */
    var _projectRoot = null;

    /**
     * Returns true if absPath lies within the project, false otherwise.
     * FIXME: Does not support paths containing ".."
     */
    function isWithinProject(absPath) {
        var rootPath = _projectRoot.fullPath;
        if (rootPath.charAt(rootPath.length - 1) != "/") {  // TODO: standardize whether DirectoryEntry.fullPath can end in "/"
            rootPath += "/";
        }
        return (absPath.indexOf(rootPath) == 0);
    }
    /**
     * If absPath lies within the project, returns a project-relative path. Else returns absPath
     * unmodified.
     * FIXME: Does not support paths containing ".."
     */
    function makeProjectRelativeIfPossible(absPath) {
        if (isWithinProject(absPath)) {
            var relPath = absPath.slice(_projectRoot.fullPath.length);
            if (relPath.charAt(0) == '/') {  // TODO: standardize whether DirectoryEntry.fullPath can end in "/"
                relPath = relPath.slice(1);
            }
            return relPath;
        }
        return absPath;
    }

    /**
     * @private
     * Reference to the tree control
     * @type {jQueryObject}
     */
    var _projectTree = null;

    /**
     * @private
     * Used to initialize jstree state
     */
    var _projectInitialLoad =
        { previous : []
        , id : 0
        , fullPathToIdMap : {}
        };

    /**
     * @private
     * Preferences callback. Saves current project path.
     */
    function _savePreferences( storage ) {
        // save the current project
        storage.projectPath = _projectRoot.fullPath;

        // save jstree state
        var openNodes = []
        ,   projectPathLength = _projectRoot.fullPath.length + 1
        ,   entry
        ,   fullPath
        ,   shortPath
        ,   depth;

        // Query open nodes by class selector
        $(".jstree-open").each( function ( index ) {
            entry = $( this ).data("entry");

            if ( entry.fullPath ) {
                fullPath = entry.fullPath;

                // Truncate project path prefix
                shortPath = fullPath.slice( projectPathLength );

                // Determine depth of the node by counting path separators.
                // Children at the root have depth of zero.
                depth = shortPath.split("/").length - 1;

                // Map tree depth to list of open nodes
                if ( openNodes[ depth ] === undefined ) {
                    openNodes[ depth ] = [];
                }

                openNodes[ depth ].push( fullPath );
            }
        });

        // Store the open nodes by their full path and persist to storage
        storage.projectTreeState = openNodes;
    }

    /**
     * Displays a browser dialog where the user can choose a folder to load.
     * (If the user cancels the dialog, nothing more happens).
     */
    function openProject() {
        // Confirm any unsaved changes first. We run the command in "prompt-only" mode, meaning it won't
        // actually close any documents even on success; we'll do that manually after the user also oks
        //the folder-browse dialog.
        CommandManager.execute(Commands.FILE_CLOSE_ALL, true)
        .done(function() {
            // Pop up a folder browse dialog
            NativeFileSystem.showOpenDialog(false, true, "Choose a folder", null, null,
                function(files) {
                    // If length == 0, user canceled the dialog; length should never be > 1
                    if (files.length > 0) {
                        // Actually close all the old files now that we know for sure we're proceeding
                        DocumentManager.closeAll();

                        // Load the new project into the folder tree
                        loadProject( files[0] );
                    }
                },
                function(error) {
                    brackets.showModalDialog(
                          brackets.DIALOG_ID_ERROR
                        , Strings.ERROR_LOADING_PROJECT
                        , Strings.format(Strings.OPEN_DIALOG_ERROR, error.code)
                    );
                }
            );
        });
        // if fail, don't open new project: user canceled (or we failed to save its unsaved changes)
    }

    /**
     * Loads the given folder as a project. Normally, you would call openProject() instead to let the
     * user choose a folder.
     *
     * @param {string} rootPath  Absolute path to the root folder of the project.
     *  If rootPath is undefined or null, the last open project will be restored.
     * @return {Deferred} A $.Deferred() object that will be resolved when the
     *  project is loaded and tree is rendered, or rejected if the project path
     *  fails to load.
     */
    function loadProject(rootPath) {
        // reset tree node id's
        _projectInitialLoad.id = 0;

        var prefs = PreferencesManager.getPreferences(PREFERENCES_CLIENT_ID)
        ,   result = new $.Deferred()
        ,   resultRenderTree
        ,   isFirstProjectOpen = false;

        if (rootPath === null || rootPath === undefined) {
            // Load the last known project into the tree
            rootPath = prefs.projectPath;
            isFirstProjectOpen = true;

            // TODO (jasonsj): handle missing paths, see issue #100
            _projectInitialLoad.previous = prefs.projectTreeState;

            if (brackets.inBrowser) {
                // In browser: dummy folder tree (hardcoded in ProjectManager)
               rootPath = "DummyProject";
            }
        }

        // Set title
        var projectName = rootPath.substring(rootPath.lastIndexOf("/") + 1);
        $("#project-title").html(projectName);

        // Populate file tree
        if (brackets.inBrowser) {
            // Hardcoded dummy data for local testing, in jsTree JSON format
            // (we leave _projectRoot null)
            var subfolderInner = { data:"Folder_inner", children:[
                { data: "subsubfile_1" }, { data: "subsubfile_2" }
            ] };
            var treeJSONData = [
                { data: "Dummy tree content:" },
                { data:"Folder_1", children:[
                    { data: "subfile_1" }, { data: "subfile_2" }, { data: "subfile_3" }
                ] },
                { data:"Folder_2", children:[
                    { data: "subfile_4" }, subfolderInner, { data: "subfile_5" }
                ] },
                { data: "file_1" },
                { data: "file_2" }
            ];

            // Show file list in UI
            resultRenderTree = _renderTree(treeJSONData, result);

        } else {
            // Point at a real folder structure on local disk
            NativeFileSystem.requestNativeFileSystem(rootPath,
                function(rootEntry) {
                    // Success!
                    _projectRoot = rootEntry;

                    // The tree will invoke our "data provider" function to populate the top-level items, then
                    // go idle until a node is expanded - at which time it'll call us again to fetch the node's
                    // immediate children, and so on.
                    resultRenderTree = _renderTree(_treeDataProvider);
                },
                function(error) {
                    brackets.showModalDialog(
                          brackets.DIALOG_ID_ERROR
                        , Strings.ERROR_LOADING_PROJECT
                        , Strings.format(Strings.REQUEST_NATIVE_FILE_SYSTEM_ERROR, rootPath, error.code
                        , function() { result.reject(); })
                    );
                }
            );
        }

        resultRenderTree.done(function () {
            result.resolve();

            if (isFirstProjectOpen) {
                $(exports).triggerHandler("isFirstProjectOpen", _projectRoot);
            }
        });
        resultRenderTree.fail(function () {
            result.reject();
        });

        return result;
    }

    /**
     * Returns the FileEntry or DirectoryEntry corresponding to the selected item, or null
     * if no item is selected.
     *
     * @return {?Entry}
     */
    function getSelectedItem() {
        var selected = _projectTree.jstree("get_selected");
        if (selected)
            return selected.data("entry");
        return null;
    }

    /**
     * Create a new item in the project tree.
     *
     * @param baseDir {string} Full path of the directory where the item should go
     * @param initialName {string} Initial name for the item
     * @param skipRename {boolean} If true, don't allow the user to rename the item
     * @return {Deferred} A $.Deferred() object that will be resolved with the FileEntry
     *  of the created object, or rejected if the user cancelled or entered an illegal
     *  filename.
     */
    function createNewItem(baseDir, initialName, skipRename) {
        // TODO: Need API to get tree node for baseDir.
        // In the meantime, pass null for node so new item is placed
        // relative to the selection
        var node = null,
            selection = _projectTree.jstree("get_selected"),
            selectionEntry = null,
            position = "inside",
            escapeKeyPressed = false,
            result = new $.Deferred();

        // get the FileEntry or DirectoryEntry
        if ( selection ) {
            selectionEntry = selection.data("entry")
        }

        // move selection to parent DirectoryEntry
        if ( selectionEntry && selectionEntry.isFile ) {
            position = "after";

            // FIXME (jasonsj): get_parent returns the tree instead of the directory?
            /*
            selection = _projectTree.jstree("get_parent", selection);

            if ( typeof( selection.data ) == "function" ) {
                // get Entry from tree node
                // note that the jstree root will return undefined
                selectionEntry = selection.data("entry");
            }
            else {
                // reset here. will be replaced with project root.
                selectionEntry = null;
            }
            */
            // FIXME (jasonsj): hackish way to get parent directory; replace with Entry.getParent() when available
            var filePath = selectionEntry.fullPath;
            selectionEntry = new NativeFileSystem.DirectoryEntry(filePath.substring(0, filePath.lastIndexOf("/")));
        }

        // use the project root DirectoryEntry
        if ( !selectionEntry ) {
            selectionEntry = getProjectRoot();
        }

        _projectTree.on("create.jstree", function(event, data) {
            $(event.target).off("create.jstree");

            function errorCleanup() {
                // TODO: If an error occurred, we should allow the user to fix the filename.
                // For now we just remove the node so you have to start again.
                _projectTree.jstree("remove", data.rslt.obj);
                result.reject();
            }

            if (!escapeKeyPressed) {
                // Validate file name
                // TODO: There are some filenames like COM1, LPT3, etc. that are not valid on Windows.
                // We may want to add checks for those here.
                // See http://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx
                if (data.rslt.name.search(/[/?*:;{}<>\\|]+/) !== -1) {
                    brackets.showModalDialog(
                            brackets.DIALOG_ID_ERROR
                        ,   Strings.INVALID_FILENAME_TITLE
                        ,   Strings.INVALID_FILENAME_MESSAGE);

                    errorCleanup();
                    return;
                }

                // Use getFile() to create the new file
                selectionEntry.getFile(data.rslt.name
                    , {create: true, exclusive: true}
                    , function( entry ) {
                        data.rslt.obj.data("entry", entry);
                        _projectTree.jstree("select_node", data.rslt.obj, true);
                        result.resolve(entry);
                    }
                    , function ( error ) {
                        if ( ( error.code === FileError.PATH_EXISTS_ERR )
                             || (error.code === FileError.TYPE_MISMATCH_ERR ) ) {
                            brackets.showModalDialog(
                                  brackets.DIALOG_ID_ERROR
                                , Strings.INVALID_FILENAME_TITLE
                                , Strings.format(
                                      Strings.FILE_ALREADY_EXISTS
                                    , data.rslt.name
                                )
                            );
                        }
                        else {
                            var errString = error.code == FileError.NO_MODIFICATION_ALLOWED_ERR ?
                                             Strings.NO_MODIFICATION_ALLOWED_ERR :
                                             Strings.format(String.GENERIC_ERROR, error.code)
                            var errMsg = Strings.format(Strings.ERROR_CREATING_FILE, data.rslt.name, errString);

                            brackets.showModalDialog(
                                  brackets.DIALOG_ID_ERROR
                                , Strings.ERROR_CREATING_FILE_TITLE
                                , errMsg
                            );
                        }

                        errorCleanup();
                    }
                );
            }
            else { //escapeKeyPressed
                errorCleanup();
            }
        });

        // Create the node and open the editor
        _projectTree.jstree("create", node, position, {data: initialName}, null, skipRename);

        var renameInput = _projectTree.find(".jstree-rename-input");

        renameInput.on("keydown", function(event) {
            // Listen for escape key on keydown, so we can remove the node in the create.jstree handler above
            if (event.keyCode == 27) {
                escapeKeyPressed = true;
            }
        });

        // TODO: Figure out better way to style this input. All styles are inlined by jsTree...
        renameInput.css(
            { left: "17px"
            , height: "24px"
            }
        ).parent().css(
            { height: "26px"
            }
        );

        return result;
    }

    /**
     * @private
     * Called by jsTree when the user has expanded a node that has never been expanded before. We call
     * jsTree back asynchronously with the node's immediate children data once the subfolder is done
     * being fetched.
     *
     * @param {jQueryObject} treeNode  jQ object for the DOM node being expanded
     * @param {function(Array)} jsTreeCallback  jsTree callback to provide children to
     */
    function _treeDataProvider(treeNode, jsTreeCallback) {
        var dirEntry;

        if (treeNode == -1) {
            // Special case: root of tree
            dirEntry = _projectRoot;
        } else {
            // All other nodes: the DirectoryEntry is saved as jQ data in the tree (by _convertEntriesToJSON())
            dirEntry = treeNode.data("entry");
        }

        // Fetch dirEntry's contents
        dirEntry.createReader().readEntries(
            function(entries) {
                var subtreeJSON = _convertEntriesToJSON(entries);
                //If the list is empty, add an empty object so the loading message goes away
                if( subtreeJSON.length === 0 )
                    subtreeJSON.push({});
                jsTreeCallback(subtreeJSON);
            },
            function(error) {
                brackets.showModalDialog(
                      brackets.DIALOG_ID_ERROR
                    , Strings.ERROR_LOADING_PROJECT
                    , Strings.format(Strings.READ_DIRECTORY_ENTRIES_ERROR, dirEntry.fullPath, error.code)
                );
            }
        );

    }

    /**
     * @private
     * Given an array of NativeFileSystem entries, returns a JSON array representing them in the format
     * required by jsTree. Saves the corresponding Entry object as metadata (which jsTree will store in
     * the DOM via $.data()).
     *
     * Does NOT recursively traverse the file system: folders are marked as expandable but are given no
     * children initially.
     *
     * @param {Array.<Entry>} entries  Array of NativeFileSystem entry objects.
     * @return {Array} jsTree node data: array of JSON objects
     */
    function _convertEntriesToJSON(entries) {
        var jsonEntryList = []
        ,   entry;

        for (var entryI in entries) {
            entry = entries[entryI];

            var jsonEntry = {
                data: entry.name,
                attr: { id: "node" + _projectInitialLoad.id++ },
                metadata: { entry: entry }
            };
            if (entry.isDirectory) {
                jsonEntry.children = [];
                jsonEntry.state = "closed";
            }

            // For more info on jsTree's JSON format see: http://www.jstree.com/documentation/json_data
            jsonEntryList.push(jsonEntry);

            // Map path to ID to initialize loaded and opened states
            _projectInitialLoad.fullPathToIdMap[entry.fullPath] = jsonEntry.attr.id;
        }
        return jsonEntryList;
    }

    /**
     * @private
     * Given an input to jsTree's json_data.data setting, display the data in the file tree UI
     * (replacing any existing file tree that was previously displayed). This input could be
     * raw JSON data, or it could be a dataprovider function. See jsTree docs for details:
     * http://www.jstree.com/documentation/json_data
     */
    function _renderTree(treeDataProvider) {
        var projectTreeContainer = $("#project-files-container")
        ,   result = new $.Deferred();

        // Instantiate tree widget
        // (jsTree is smart enough to replace the old tree if there's already one there)
        _projectTree = projectTreeContainer.jstree({
            plugins : ["ui", "themes", "json_data", "crrm"],
            json_data : { data:treeDataProvider, correct_state: false },
            core : { animation:0 },
            themes : { theme:"brackets", url:"styles/jsTreeTheme.css", dots:false, icons:false },
                //(note: our actual jsTree theme CSS lives in brackets.less; we specify an empty .css
                // file because jsTree insists on loading one itself)

            strings : { loading : "Loading ...", new_node : "New node" }    // TODO: localization
        })
        .bind("select_node.jstree", function(event, data) {
            var entry = data.rslt.obj.data("entry");
            if (entry.isFile)
                CommandManager.execute(Commands.FILE_OPEN, entry.fullPath);
        })
        .bind("reopen.jstree", function(event, data) {
            // This handler fires for the initial load and subsequent
            // reload_nodes events. For each depth level of the tree, we open
            // the saved nodes by a fullPath lookup.
            if ( _projectInitialLoad.previous.length > 0 ) {
                // load previously open nodes by increasing depth
                var toOpenPaths = _projectInitialLoad.previous.shift()
                ,   toOpenIds   = [];

                // use path to lookup ID
                $.each( toOpenPaths, function(index, value) {
                    toOpenIds.push(_projectInitialLoad.fullPathToIdMap[value]);
                });

                // specify nodes to open and load
                data.inst.data.core.to_open = toOpenIds;
                _projectTree.jstree("reload_nodes", false);
            }
            if ( _projectInitialLoad.previous.length === 0 ) {
                result.resolve();
            }
        })
        .bind("dblclick.jstree", function(event) {
            var entry = $(event.target).closest("li").data("entry");
            if (entry.isFile) {
                CommandManager.execute(Commands.FILE_ADD_TO_WORKING_SET, entry.fullPath);
            }
        });

        return result;
    };

    // Define public API
    exports.getProjectRoot  = getProjectRoot;
    exports.isWithinProject = isWithinProject;
    exports.makeProjectRelativeIfPossible = makeProjectRelativeIfPossible;
    exports.openProject     = openProject;
    exports.loadProject     = loadProject;
    exports.getSelectedItem = getSelectedItem;
    exports.createNewItem   = createNewItem;

    // Initialize now
    (function() {
        var loadedPath = window.location.pathname;
        var bracketsSrc = loadedPath.substr(0, loadedPath.lastIndexOf("/"));
        var defaults =
            { projectPath:      bracketsSrc /* initialze to brackets source */
            , projectTreeState: ""          /* TODO (jasonsj): jstree state */
            };
        PreferencesManager.addPreferencesClient(PREFERENCES_CLIENT_ID, _savePreferences, this, defaults);
    })();
});


jasmine.HtmlReporterHelpers = {};

jasmine.HtmlReporterHelpers.createDom = function(type, attrs, childrenVarArgs) {
  var el = document.createElement(type);

  for (var i = 2; i < arguments.length; i++) {
    var child = arguments[i];

    if (typeof child === 'string') {
      el.appendChild(document.createTextNode(child));
    } else {
      if (child) {
        el.appendChild(child);
      }
    }
  }

  for (var attr in attrs) {
    if (attr == "className") {
      el[attr] = attrs[attr];
    } else {
      el.setAttribute(attr, attrs[attr]);
    }
  }

  return el;
};

jasmine.HtmlReporterHelpers.getSpecStatus = function(child) {
  var results = child.results();
  var status = results.passed() ? 'passed' : 'failed';
  if (results.skipped) {
    status = 'skipped';
  }

  return status;
};

jasmine.HtmlReporterHelpers.appendToSummary = function(child, childElement) {
  var parentDiv = this.dom.summary;
  var parentSuite = (typeof child.parentSuite == 'undefined') ? 'suite' : 'parentSuite';
  var parent = child[parentSuite];

  if (parent) {
    if (typeof this.views.suites[parent.id] == 'undefined') {
      this.views.suites[parent.id] = new jasmine.HtmlReporter.SuiteView(parent, this.dom, this.views);
    }
    parentDiv = this.views.suites[parent.id].element;
  }

  parentDiv.appendChild(childElement);
};

jasmine.HtmlReporterHelpers.addHelpers = function(ctor) {
  for(var fn in jasmine.HtmlReporterHelpers) {
    ctor.prototype[fn] = jasmine.HtmlReporterHelpers[fn];
  }
};

jasmine.HtmlReporter = function(_doc) {
  var self = this;
  var doc = _doc || window.document;

  var reporterView;

  var dom = {};

  // Jasmine Reporter Public Interface
  self.logRunningSpecs = false;

  self.reportRunnerStarting = function(runner) {
    var specs = runner.specs() || [];

    if (specs.length == 0) {
      return;
    }

    createReporterDom(runner.env.versionString());
    doc.body.appendChild(dom.reporter);

    reporterView = new jasmine.HtmlReporter.ReporterView(dom);
    reporterView.addSpecs(specs, self.specFilter);
  };

  self.reportRunnerResults = function(runner) {
    reporterView.complete();
  };

  self.reportSuiteResults = function(suite) {
    reporterView.suiteComplete(suite);
  };

  self.reportSpecStarting = function(spec) {
    if (self.logRunningSpecs) {
      self.log('>> Jasmine Running ' + spec.suite.description + ' ' + spec.description + '...');
    }
  };

  self.reportSpecResults = function(spec) {
    reporterView.specComplete(spec);
  };

  self.log = function() {
    var console = jasmine.getGlobal().console;
    if (console && console.log) {
      if (console.log.apply) {
        console.log.apply(console, arguments);
      } else {
        console.log(arguments); // ie fix: console.log.apply doesn't exist on ie
      }
    }
  };

  self.specFilter = function(spec) {
    if (!focusedSpecName()) {
      return true;
    }

    return spec.getFullName().indexOf(focusedSpecName()) === 0;
  };

  return self;

  function focusedSpecName() {
    var specName;

    (function memoizeFocusedSpec() {
      if (specName) {
        return;
      }

      var paramMap = [];
      var params = doc.location.search.substring(1).split('&');

      for (var i = 0; i < params.length; i++) {
        var p = params[i].split('=');
        paramMap[decodeURIComponent(p[0])] = decodeURIComponent(p[1]);
      }

      specName = paramMap.spec;
    })();

    return specName;
  }

  function createReporterDom(version) {
    dom.reporter = self.createDom('div', { id: 'HTMLReporter', className: 'jasmine_reporter' },
      dom.banner = self.createDom('div', { className: 'banner' },
        self.createDom('span', { className: 'title' }, "Jasmine "),
        self.createDom('span', { className: 'version' }, version)),

      dom.symbolSummary = self.createDom('ul', {className: 'symbolSummary'}),
      dom.alert = self.createDom('div', {className: 'alert'}),
      dom.results = self.createDom('div', {className: 'results'},
        dom.summary = self.createDom('div', { className: 'summary' }),
        dom.details = self.createDom('div', { id: 'details' }))
    );
  }
};
jasmine.HtmlReporterHelpers.addHelpers(jasmine.HtmlReporter);jasmine.HtmlReporterHelpers = {};

jasmine.HtmlReporterHelpers.createDom = function(type, attrs, childrenVarArgs) {
  var el = document.createElement(type);

  for (var i = 2; i < arguments.length; i++) {
    var child = arguments[i];

    if (typeof child === 'string') {
      el.appendChild(document.createTextNode(child));
    } else {
      if (child) {
        el.appendChild(child);
      }
    }
  }

  for (var attr in attrs) {
    if (attr == "className") {
      el[attr] = attrs[attr];
    } else {
      el.setAttribute(attr, attrs[attr]);
    }
  }

  return el;
};

jasmine.HtmlReporterHelpers.getSpecStatus = function(child) {
  var results = child.results();
  var status = results.passed() ? 'passed' : 'failed';
  if (results.skipped) {
    status = 'skipped';
  }

  return status;
};

jasmine.HtmlReporterHelpers.appendToSummary = function(child, childElement) {
  var parentDiv = this.dom.summary;
  var parentSuite = (typeof child.parentSuite == 'undefined') ? 'suite' : 'parentSuite';
  var parent = child[parentSuite];

  if (parent) {
    if (typeof this.views.suites[parent.id] == 'undefined') {
      this.views.suites[parent.id] = new jasmine.HtmlReporter.SuiteView(parent, this.dom, this.views);
    }
    parentDiv = this.views.suites[parent.id].element;
  }

  parentDiv.appendChild(childElement);
};

jasmine.HtmlReporterHelpers.addHelpers = function(ctor) {
  for(var fn in jasmine.HtmlReporterHelpers) {
    ctor.prototype[fn] = jasmine.HtmlReporterHelpers[fn];
  }
};

jasmine.HtmlReporter.ReporterView = function(dom) {
  this.startedAt = new Date();
  this.runningSpecCount = 0;
  this.completeSpecCount = 0;
  this.passedCount = 0;
  this.failedCount = 0;
  this.skippedCount = 0;

  this.createResultsMenu = function() {
    this.resultsMenu = this.createDom('span', {className: 'resultsMenu bar'},
      this.summaryMenuItem = this.createDom('a', {className: 'summaryMenuItem', href: "#"}, '0 specs'),
      ' | ',
      this.detailsMenuItem = this.createDom('a', {className: 'detailsMenuItem', href: "#"}, '0 failing'));

    this.summaryMenuItem.onclick = function() {
      dom.reporter.className = dom.reporter.className.replace(/ showDetails/g, '');
    };

    this.detailsMenuItem.onclick = function() {
      showDetails();
    };
  };

  this.addSpecs = function(specs, specFilter) {
    this.totalSpecCount = specs.length;

    this.views = {
      specs: {},
      suites: {}
    };

    for (var i = 0; i < specs.length; i++) {
      var spec = specs[i];
      this.views.specs[spec.id] = new jasmine.HtmlReporter.SpecView(spec, dom, this.views);
      if (specFilter(spec)) {
        this.runningSpecCount++;
      }
    }
  };

  this.specComplete = function(spec) {
    this.completeSpecCount++;

    if (isUndefined(this.views.specs[spec.id])) {
      this.views.specs[spec.id] = new jasmine.HtmlReporter.SpecView(spec, dom);
    }

    var specView = this.views.specs[spec.id];

    switch (specView.status()) {
      case 'passed':
        this.passedCount++;
        break;

      case 'failed':
        this.failedCount++;
        break;

      case 'skipped':
        this.skippedCount++;
        break;
    }

    specView.refresh();
    this.refresh();
  };

  this.suiteComplete = function(suite) {
    var suiteView = this.views.suites[suite.id];
    if (isUndefined(suiteView)) {
      return;
    }
    suiteView.refresh();
  };

  this.refresh = function() {

    if (isUndefined(this.resultsMenu)) {
      this.createResultsMenu();
    }

    // currently running UI
    if (isUndefined(this.runningAlert)) {
      this.runningAlert = this.createDom('a', {href: "?", className: "runningAlert bar"});
      dom.alert.appendChild(this.runningAlert);
    }
    this.runningAlert.innerHTML = "Running " + this.completeSpecCount + " of " + specPluralizedFor(this.totalSpecCount);

    // skipped specs UI
    if (isUndefined(this.skippedAlert)) {
      this.skippedAlert = this.createDom('a', {href: "?", className: "skippedAlert bar"});
    }

    this.skippedAlert.innerHTML = "Skipping " + this.skippedCount + " of " + specPluralizedFor(this.totalSpecCount) + " - run all";

    if (this.skippedCount === 1 && isDefined(dom.alert)) {
      dom.alert.appendChild(this.skippedAlert);
    }

    // passing specs UI
    if (isUndefined(this.passedAlert)) {
      this.passedAlert = this.createDom('span', {href: "?", className: "passingAlert bar"});
    }
    this.passedAlert.innerHTML = "Passing " + specPluralizedFor(this.passedCount);

    // failing specs UI
    if (isUndefined(this.failedAlert)) {
      this.failedAlert = this.createDom('span', {href: "?", className: "failingAlert bar"});
    }
    this.failedAlert.innerHTML = "Failing " + specPluralizedFor(this.failedCount);

    if (this.failedCount === 1 && isDefined(dom.alert)) {
      dom.alert.appendChild(this.failedAlert);
      dom.alert.appendChild(this.resultsMenu);
    }

    // summary info
    this.summaryMenuItem.innerHTML = "" + specPluralizedFor(this.runningSpecCount);
    this.detailsMenuItem.innerHTML = "" + this.failedCount + " failing";
  };

  this.complete = function() {
    dom.alert.removeChild(this.runningAlert);

    this.skippedAlert.innerHTML = "Ran " + this.runningSpecCount + " of " + specPluralizedFor(this.totalSpecCount) + " - run all";

    if (this.failedCount === 0) {
      dom.alert.appendChild(this.createDom('span', {className: 'passingAlert bar'}, "Passing " + specPluralizedFor(this.passedCount)));
    } else {
      showDetails();
    }

    dom.banner.appendChild(this.createDom('span', {className: 'duration'}, "finished in " + ((new Date().getTime() - this.startedAt.getTime()) / 1000) + "s"));
  };

  return this;

  function showDetails() {
    if (dom.reporter.className.search(/showDetails/) === -1) {
      dom.reporter.className += " showDetails";
    }
  }

  function isUndefined(obj) {
    return typeof obj === 'undefined';
  }

  function isDefined(obj) {
    return !isUndefined(obj);
  }

  function specPluralizedFor(count) {
    var str = count + " spec";
    if (count > 1) {
      str += "s"
    }
    return str;
  }

};

jasmine.HtmlReporterHelpers.addHelpers(jasmine.HtmlReporter.ReporterView);

jasmine.HtmlReporter.SpecView = function(spec, dom, views) {
  this.spec = spec;
  this.dom = dom;
  this.views = views;

  this.symbol = this.createDom('li', { className: 'pending' });
  this.dom.symbolSummary.appendChild(this.symbol);

  this.summary = this.createDom('div', { className: 'specSummary' },
      this.createDom('a', {
        className: 'description',
        href: '?spec=' + encodeURIComponent(this.spec.getFullName()),
        title: this.spec.getFullName()
      }, this.spec.description)
  );

  this.detail = this.createDom('div', { className: 'specDetail' },
      this.createDom('a', {
        className: 'description',
        href: '?spec=' + encodeURIComponent(this.spec.getFullName()),
        title: this.spec.getFullName()
      }, this.spec.getFullName())
  );
};

jasmine.HtmlReporter.SpecView.prototype.status = function() {
  return this.getSpecStatus(this.spec);
};

jasmine.HtmlReporter.SpecView.prototype.refresh = function() {
  this.symbol.className = this.status();

  switch (this.status()) {
    case 'skipped':
      break;

    case 'passed':
      this.appendSummaryToSuiteDiv();
      break;

    case 'failed':
      this.appendSummaryToSuiteDiv();
      this.appendFailureDetail();
      break;
  }
};

jasmine.HtmlReporter.SpecView.prototype.appendSummaryToSuiteDiv = function() {
  this.summary.className += ' ' + this.status();
  this.appendToSummary(this.spec, this.summary);
};

jasmine.HtmlReporter.SpecView.prototype.appendFailureDetail = function() {
  this.detail.className += ' ' + this.status();

  var resultItems = this.spec.results().getItems();
  var messagesDiv = this.createDom('div', { className: 'messages' });

  for (var i = 0; i < resultItems.length; i++) {
    var result = resultItems[i];

    if (result.type == 'log') {
      messagesDiv.appendChild(this.createDom('div', {className: 'resultMessage log'}, result.toString()));
    } else if (result.type == 'expect' && result.passed && !result.passed()) {
      messagesDiv.appendChild(this.createDom('div', {className: 'resultMessage fail'}, result.message));

      if (result.trace.stack) {
        messagesDiv.appendChild(this.createDom('div', {className: 'stackTrace'}, result.trace.stack));
      }
    }
  }

  if (messagesDiv.childNodes.length > 0) {
    this.detail.appendChild(messagesDiv);
    this.dom.details.appendChild(this.detail);
  }
};

jasmine.HtmlReporterHelpers.addHelpers(jasmine.HtmlReporter.SpecView);jasmine.HtmlReporter.SuiteView = function(suite, dom, views) {
  this.suite = suite;
  this.dom = dom;
  this.views = views;

  this.element = this.createDom('div', { className: 'suite' },
      this.createDom('a', { className: 'description', href: '?spec=' + encodeURIComponent(this.suite.getFullName()) }, this.suite.description)
  );

  this.appendToSummary(this.suite, this.element);
};

jasmine.HtmlReporter.SuiteView.prototype.status = function() {
  return this.getSpecStatus(this.suite);
};

jasmine.HtmlReporter.SuiteView.prototype.refresh = function() {
  this.element.className += " " + this.status();
};

jasmine.HtmlReporterHelpers.addHelpers(jasmine.HtmlReporter.SuiteView);

/* @deprecated Use jasmine.HtmlReporter instead
 */
jasmine.TrivialReporter = function(doc) {
  this.document = doc || document;
  this.suiteDivs = {};
  this.logRunningSpecs = false;
};

jasmine.TrivialReporter.prototype.createDom = function(type, attrs, childrenVarArgs) {
  var el = document.createElement(type);

  for (var i = 2; i < arguments.length; i++) {
    var child = arguments[i];

    if (typeof child === 'string') {
      el.appendChild(document.createTextNode(child));
    } else {
      if (child) { el.appendChild(child); }
    }
  }

  for (var attr in attrs) {
    if (attr == "className") {
      el[attr] = attrs[attr];
    } else {
      el.setAttribute(attr, attrs[attr]);
    }
  }

  return el;
};

jasmine.TrivialReporter.prototype.reportRunnerStarting = function(runner) {
  var showPassed, showSkipped;

  this.outerDiv = this.createDom('div', { id: 'TrivialReporter', className: 'jasmine_reporter' },
      this.createDom('div', { className: 'banner' },
        this.createDom('div', { className: 'logo' },
            this.createDom('span', { className: 'title' }, "Jasmine"),
            this.createDom('span', { className: 'version' }, runner.env.versionString())),
        this.createDom('div', { className: 'options' },
            "Show ",
            showPassed = this.createDom('input', { id: "__jasmine_TrivialReporter_showPassed__", type: 'checkbox' }),
            this.createDom('label', { "for": "__jasmine_TrivialReporter_showPassed__" }, " passed "),
            showSkipped = this.createDom('input', { id: "__jasmine_TrivialReporter_showSkipped__", type: 'checkbox' }),
            this.createDom('label', { "for": "__jasmine_TrivialReporter_showSkipped__" }, " skipped")
            )
          ),

      this.runnerDiv = this.createDom('div', { className: 'runner running' },
          this.createDom('a', { className: 'run_spec', href: '?' }, "run all"),
          this.runnerMessageSpan = this.createDom('span', {}, "Running..."),
          this.finishedAtSpan = this.createDom('span', { className: 'finished-at' }, ""))
      );

  this.document.body.appendChild(this.outerDiv);

  var suites = runner.suites();
  for (var i = 0; i < suites.length; i++) {
    var suite = suites[i];
    var suiteDiv = this.createDom('div', { className: 'suite' },
        this.createDom('a', { className: 'run_spec', href: '?spec=' + encodeURIComponent(suite.getFullName()) }, "run"),
        this.createDom('a', { className: 'description', href: '?spec=' + encodeURIComponent(suite.getFullName()) }, suite.description));
    this.suiteDivs[suite.id] = suiteDiv;
    var parentDiv = this.outerDiv;
    if (suite.parentSuite) {
      parentDiv = this.suiteDivs[suite.parentSuite.id];
    }
    parentDiv.appendChild(suiteDiv);
  }

  this.startedAt = new Date();

  var self = this;
  showPassed.onclick = function(evt) {
    if (showPassed.checked) {
      self.outerDiv.className += ' show-passed';
    } else {
      self.outerDiv.className = self.outerDiv.className.replace(/ show-passed/, '');
    }
  };

  showSkipped.onclick = function(evt) {
    if (showSkipped.checked) {
      self.outerDiv.className += ' show-skipped';
    } else {
      self.outerDiv.className = self.outerDiv.className.replace(/ show-skipped/, '');
    }
  };
};

jasmine.TrivialReporter.prototype.reportRunnerResults = function(runner) {
  var results = runner.results();
  var className = (results.failedCount > 0) ? "runner failed" : "runner passed";
  this.runnerDiv.setAttribute("class", className);
  //do it twice for IE
  this.runnerDiv.setAttribute("className", className);
  var specs = runner.specs();
  var specCount = 0;
  for (var i = 0; i < specs.length; i++) {
    if (this.specFilter(specs[i])) {
      specCount++;
    }
  }
  var message = "" + specCount + " spec" + (specCount == 1 ? "" : "s" ) + ", " + results.failedCount + " failure" + ((results.failedCount == 1) ? "" : "s");
  message += " in " + ((new Date().getTime() - this.startedAt.getTime()) / 1000) + "s";
  this.runnerMessageSpan.replaceChild(this.createDom('a', { className: 'description', href: '?'}, message), this.runnerMessageSpan.firstChild);

  this.finishedAtSpan.appendChild(document.createTextNode("Finished at " + new Date().toString()));
};

jasmine.TrivialReporter.prototype.reportSuiteResults = function(suite) {
  var results = suite.results();
  var status = results.passed() ? 'passed' : 'failed';
  if (results.totalCount === 0) { // todo: change this to check results.skipped
    status = 'skipped';
  }
  this.suiteDivs[suite.id].className += " " + status;
};

jasmine.TrivialReporter.prototype.reportSpecStarting = function(spec) {
  if (this.logRunningSpecs) {
    this.log('>> Jasmine Running ' + spec.suite.description + ' ' + spec.description + '...');
  }
};

jasmine.TrivialReporter.prototype.reportSpecResults = function(spec) {
  var results = spec.results();
  var status = results.passed() ? 'passed' : 'failed';
  if (results.skipped) {
    status = 'skipped';
  }
  var specDiv = this.createDom('div', { className: 'spec '  + status },
      this.createDom('a', { className: 'run_spec', href: '?spec=' + encodeURIComponent(spec.getFullName()) }, "run"),
      this.createDom('a', {
        className: 'description',
        href: '?spec=' + encodeURIComponent(spec.getFullName()),
        title: spec.getFullName()
      }, spec.description));

  var resultItems = results.getItems();
  var messagesDiv = this.createDom('div', { className: 'messages' });
  for (var i = 0; i < resultItems.length; i++) {
    var result = resultItems[i];

    if (result.type == 'log') {
      messagesDiv.appendChild(this.createDom('div', {className: 'resultMessage log'}, result.toString()));
    } else if (result.type == 'expect' && result.passed && !result.passed()) {
      messagesDiv.appendChild(this.createDom('div', {className: 'resultMessage fail'}, result.message));

      if (result.trace.stack) {
        messagesDiv.appendChild(this.createDom('div', {className: 'stackTrace'}, result.trace.stack));
      }
    }
  }

  if (messagesDiv.childNodes.length > 0) {
    specDiv.appendChild(messagesDiv);
  }

  this.suiteDivs[spec.suite.id].appendChild(specDiv);
};

jasmine.TrivialReporter.prototype.log = function() {
  var console = jasmine.getGlobal().console;
  if (console && console.log) {
    if (console.log.apply) {
      console.log.apply(console, arguments);
    } else {
      console.log(arguments); // ie fix: console.log.apply doesn't exist on ie
    }
  }
};

jasmine.TrivialReporter.prototype.getLocation = function() {
  return this.document.location;
};

jasmine.TrivialReporter.prototype.specFilter = function(spec) {
  var paramMap = {};
  var params = this.getLocation().search.substring(1).split('&');
  for (var i = 0; i < params.length; i++) {
    var p = params[i].split('=');
    paramMap[decodeURIComponent(p[0])] = decodeURIComponent(p[1]);
  }

  if (!paramMap.spec) {
    return true;
  }
  return spec.getFullName().indexOf(paramMap.spec) === 0;
};


body { background-color: #eeeeee; padding: 0; margin: 5px; overflow-y: scroll; }

#HTMLReporter { font-size: 11px; font-family: Monaco, "Lucida Console", monospace; line-height: 14px; color: #333333; }
#HTMLReporter a { text-decoration: none; }
#HTMLReporter a:hover { text-decoration: underline; }
#HTMLReporter p, #HTMLReporter h1, #HTMLReporter h2, #HTMLReporter h3, #HTMLReporter h4, #HTMLReporter h5, #HTMLReporter h6 { margin: 0; line-height: 14px; }
#HTMLReporter .banner, #HTMLReporter .symbolSummary, #HTMLReporter .summary, #HTMLReporter .resultMessage, #HTMLReporter .specDetail .description, #HTMLReporter .alert .bar, #HTMLReporter .stackTrace { padding-left: 9px; padding-right: 9px; }
#HTMLReporter #jasmine_content { position: fixed; right: 100%; }
#HTMLReporter .version { color: #aaaaaa; }
#HTMLReporter .banner { margin-top: 14px; }
#HTMLReporter .duration { color: #aaaaaa; float: right; }
#HTMLReporter .symbolSummary { overflow: hidden; *zoom: 1; margin: 14px 0; }
#HTMLReporter .symbolSummary li { display: block; float: left; height: 7px; width: 14px; margin-bottom: 7px; font-size: 16px; }
#HTMLReporter .symbolSummary li.passed { font-size: 14px; }
#HTMLReporter .symbolSummary li.passed:before { color: #5e7d00; content: "\02022"; }
#HTMLReporter .symbolSummary li.failed { line-height: 9px; }
#HTMLReporter .symbolSummary li.failed:before { color: #b03911; content: "x"; font-weight: bold; margin-left: -1px; }
#HTMLReporter .symbolSummary li.skipped { font-size: 14px; }
#HTMLReporter .symbolSummary li.skipped:before { color: #bababa; content: "\02022"; }
#HTMLReporter .symbolSummary li.pending { line-height: 11px; }
#HTMLReporter .symbolSummary li.pending:before { color: #aaaaaa; content: "-"; }
#HTMLReporter .bar { line-height: 28px; font-size: 14px; display: block; color: #eee; }
#HTMLReporter .runningAlert { background-color: #666666; }
#HTMLReporter .skippedAlert { background-color: #aaaaaa; }
#HTMLReporter .skippedAlert:first-child { background-color: #333333; }
#HTMLReporter .skippedAlert:hover { text-decoration: none; color: white; text-decoration: underline; }
#HTMLReporter .passingAlert { background-color: #a6b779; }
#HTMLReporter .passingAlert:first-child { background-color: #5e7d00; }
#HTMLReporter .failingAlert { background-color: #cf867e; }
#HTMLReporter .failingAlert:first-child { background-color: #b03911; }
#HTMLReporter .results { margin-top: 14px; }
#HTMLReporter #details { display: none; }
#HTMLReporter .resultsMenu, #HTMLReporter .resultsMenu a { background-color: #fff; color: #333333; }
#HTMLReporter.showDetails .summaryMenuItem { font-weight: normal; text-decoration: inherit; }
#HTMLReporter.showDetails .summaryMenuItem:hover { text-decoration: underline; }
#HTMLReporter.showDetails .detailsMenuItem { font-weight: bold; text-decoration: underline; }
#HTMLReporter.showDetails .summary { display: none; }
#HTMLReporter.showDetails #details { display: block; }
#HTMLReporter .summaryMenuItem { font-weight: bold; text-decoration: underline; }
#HTMLReporter .summary { margin-top: 14px; }
#HTMLReporter .summary .suite .suite, #HTMLReporter .summary .specSummary { margin-left: 14px; }
#HTMLReporter .summary .specSummary.passed a { color: #5e7d00; }
#HTMLReporter .summary .specSummary.failed a { color: #b03911; }
#HTMLReporter .description + .suite { margin-top: 0; }
#HTMLReporter .suite { margin-top: 14px; }
#HTMLReporter .suite a { color: #333333; }
#HTMLReporter #details .specDetail { margin-bottom: 28px; }
#HTMLReporter #details .specDetail .description { display: block; color: white; background-color: #b03911; }
#HTMLReporter .resultMessage { padding-top: 14px; color: #333333; }
#HTMLReporter .resultMessage span.result { display: block; }
#HTMLReporter .stackTrace { margin: 5px 0 0 0; max-height: 224px; overflow: auto; line-height: 18px; color: #666666; border: 1px solid #ddd; background: white; white-space: pre; }

#TrivialReporter { padding: 8px 13px; position: absolute; top: 0; bottom: 0; left: 0; right: 0; overflow-y: scroll; background-color: white; font-family: "Helvetica Neue Light", "Lucida Grande", "Calibri", "Arial", sans-serif; /*.resultMessage {*/ /*white-space: pre;*/ /*}*/ }
#TrivialReporter a:visited, #TrivialReporter a { color: #303; }
#TrivialReporter a:hover, #TrivialReporter a:active { color: blue; }
#TrivialReporter .run_spec { float: right; padding-right: 5px; font-size: .8em; text-decoration: none; }
#TrivialReporter .banner { color: #303; background-color: #fef; padding: 5px; }
#TrivialReporter .logo { float: left; font-size: 1.1em; padding-left: 5px; }
#TrivialReporter .logo .version { font-size: .6em; padding-left: 1em; }
#TrivialReporter .runner.running { background-color: yellow; }
#TrivialReporter .options { text-align: right; font-size: .8em; }
#TrivialReporter .suite { border: 1px outset gray; margin: 5px 0; padding-left: 1em; }
#TrivialReporter .suite .suite { margin: 5px; }
#TrivialReporter .suite.passed { background-color: #dfd; }
#TrivialReporter .suite.failed { background-color: #fdd; }
#TrivialReporter .spec { margin: 5px; padding-left: 1em; clear: both; }
#TrivialReporter .spec.failed, #TrivialReporter .spec.passed, #TrivialReporter .spec.skipped { padding-bottom: 5px; border: 1px solid gray; }
#TrivialReporter .spec.failed { background-color: #fbb; border-color: red; }
#TrivialReporter .spec.passed { background-color: #bfb; border-color: green; }
#TrivialReporter .spec.skipped { background-color: #bbb; }
#TrivialReporter .messages { border-left: 1px dashed gray; padding-left: 1em; padding-right: 1em; }
#TrivialReporter .passed { background-color: #cfc; display: none; }
#TrivialReporter .failed { background-color: #fbb; }
#TrivialReporter .skipped { color: #777; background-color: #eee; display: none; }
#TrivialReporter .resultMessage span.result { display: block; line-height: 2em; color: black; }
#TrivialReporter .resultMessage .mismatch { color: black; }
#TrivialReporter .stackTrace { white-space: pre; font-size: .8em; margin-left: 10px; max-height: 5em; overflow: auto; border: 1px inset red; padding: 1em; background: #eef; }
#TrivialReporter .finished-at { padding-left: 1em; font-size: .6em; }
#TrivialReporter.show-passed .passed, #TrivialReporter.show-skipped .skipped { display: block; }
#TrivialReporter #jasmine_content { position: fixed; right: 100%; }
#TrivialReporter .runner { border: 1px solid gray; display: block; margin: 5px 0; padding: 2px 0 2px 10px; }


/* =========================================================
 * bootstrap-modal.js v1.4.0
 * http://twitter.github.com/bootstrap/javascript.html#modal
 * =========================================================
 * Copyright 2011 Twitter, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * ========================================================= */

!function( $ ){

  "use strict"

 /* CSS TRANSITION SUPPORT (https://gist.github.com/373874)
  * ======================================================= */

  var transitionEnd

  $(document).ready(function () {

    $.support.transition = (function () {
      var thisBody = document.body || document.documentElement
        , thisStyle = thisBody.style
        , support = thisStyle.transition !== undefined || thisStyle.WebkitTransition !== undefined || thisStyle.MozTransition !== undefined || thisStyle.MsTransition !== undefined || thisStyle.OTransition !== undefined
      return support
    })()

    // set CSS transition event type
    if ( $.support.transition ) {
      transitionEnd = "TransitionEnd"
      if ( $.browser.webkit ) {
      	transitionEnd = "webkitTransitionEnd"
      } else if ( $.browser.mozilla ) {
      	transitionEnd = "transitionend"
      } else if ( $.browser.opera ) {
      	transitionEnd = "oTransitionEnd"
      }
    }

  })

 /* MODAL PUBLIC CLASS DEFINITION
  * ============================= */

  var Modal = function ( content, options ) {
    this.settings = $.extend({}, $.fn.modal.defaults, options)
    this.$element = $(content)
      .delegate('.close', 'click.modal', $.proxy(this.hide, this))

    if ( this.settings.show ) {
      this.show()
    }

    return this
  }

  Modal.prototype = {

      toggle: function () {
        return this[!this.isShown ? 'show' : 'hide']()
      }

    , show: function () {
        var that = this
        this.isShown = true
        this.$element.trigger('show')

        escape.call(this)
        backdrop.call(this, function () {
          var transition = $.support.transition && that.$element.hasClass('fade')

          that.$element
            .appendTo(document.body)
            .show()

          if (transition) {
            that.$element[0].offsetWidth // force reflow
          }

          that.$element.addClass('in')

          transition ?
            that.$element.one(transitionEnd, function () { that.$element.trigger('shown') }) :
            that.$element.trigger('shown')

        })

        return this
      }

    , hide: function (e) {
        e && e.preventDefault()

        if ( !this.isShown ) {
          return this
        }

        var that = this
        this.isShown = false

        escape.call(this)

        this.$element
          .trigger('hide')
          .removeClass('in')

        $.support.transition && this.$element.hasClass('fade') ?
          hideWithTransition.call(this) :
          hideModal.call(this)

        return this
      }

  }

 /* MODAL PRIVATE METHODS
  * ===================== */

  function hideWithTransition() {
    // firefox drops transitionEnd events :{o
    var that = this
      , timeout = setTimeout(function () {
          that.$element.unbind(transitionEnd)
          hideModal.call(that)
        }, 500)

    this.$element.one(transitionEnd, function () {
      clearTimeout(timeout)
      hideModal.call(that)
    })
  }

  function hideModal (that) {
    this.$element
      .hide()
      .trigger('hidden')

    backdrop.call(this)
  }

  function backdrop ( callback ) {
    var that = this
      , animate = this.$element.hasClass('fade') ? 'fade' : ''
    if ( this.isShown && this.settings.backdrop ) {
      var doAnimate = $.support.transition && animate

      this.$backdrop = $('<div class="modal-backdrop ' + animate + '" />')
        .appendTo(document.body)

      if ( this.settings.backdrop != 'static' ) {
        this.$backdrop.click($.proxy(this.hide, this))
      }

      if ( doAnimate ) {
        this.$backdrop[0].offsetWidth // force reflow
      }

      this.$backdrop.addClass('in')

      doAnimate ?
        this.$backdrop.one(transitionEnd, callback) :
        callback()

    } else if ( !this.isShown && this.$backdrop ) {
      this.$backdrop.removeClass('in')

      $.support.transition && this.$element.hasClass('fade')?
        this.$backdrop.one(transitionEnd, $.proxy(removeBackdrop, this)) :
        removeBackdrop.call(this)

    } else if ( callback ) {
       callback()
    }
  }

  function removeBackdrop() {
    this.$backdrop.remove()
    this.$backdrop = null
  }

  function escape() {
    var that = this
    if ( this.isShown && this.settings.keyboard ) {
      $(document).bind('keyup.modal', function ( e ) {
        if ( e.which == 27 ) {
          that.hide()
        }
      })
    } else if ( !this.isShown ) {
      $(document).unbind('keyup.modal')
    }
  }

 /* MODAL PLUGIN DEFINITION
  * ======================= */

  $.fn.modal = function ( options ) {
    var modal = this.data('modal')

    if (!modal) {

      if (typeof options == 'string') {
        options = {
          show: /show|toggle/.test(options)
        }
      }

      return this.each(function () {
        $(this).data('modal', new Modal(this, options))
      })
    }

    if ( options === true ) {
      return modal
    }

    if ( typeof options == 'string' ) {
      modal[options]()
    } else if ( modal ) {
      modal.toggle()
    }

    return this
  }

  $.fn.modal.Modal = Modal

  $.fn.modal.defaults = {
    backdrop: false
  , keyboard: false
  , show: false
  }

 /* MODAL DATA- IMPLEMENTATION
  * ========================== */

  $(document).ready(function () {
    $('body').delegate('[data-controls-modal]', 'click', function (e) {
      e.preventDefault()
      var $this = $(this).data('show', true)
      $('#' + $this.attr('data-controls-modal')).modal( $this.data() )
    })
  })

}( window.jQuery || window.ender );


boxFlex:1, boxFlexGroup:1, columnCount:1, fillOpacity:1, flex:1, flexGrow:1,

flexPositive:1, flexShrink:1, flexNegative:1, fontWeight:1, lineClamp:1, lineHeight:1,

opacity:1, order:1, orphans:1, strokeOpacity:1, widows:1, zIndex:1, zoom:1

export const NON_BUBBLING_EVENTS = ['blur', 'error', 'focus', 'load', 'resize', 'scroll'];

import { ATTR_KEY, NON_DIMENSION_PROPS } from '../constants';
import options from '../options';
import { toLowerCase, empty, falsey, isString, isFunction } from '../util';

export function ensureNodeData(node, data) {
	return node[ATTR_KEY] || (node[ATTR_KEY] = (data || {}));
}

export function getNodeType(node) {
	if (node instanceof Text) return 3;
	if (node instanceof Element) return 1;
	return 0;
}

/** Removes a given DOM Node from its parent. */
export function removeNode(node) {
	let p = node.parentNode;
	if (p) p.removeChild(node);
}

/** Set a named attribute on the given Node, with special behavior for some names and event handlers.
 *	If `value` is `null`, the attribute/handler will be removed.
 *	@param {Element} node	An element to mutate
 *	@param {string} name	The name/key to set, such as an event or attribute name
 *	@param {any} value		An attribute value, such as a function to be used as an event handler
 *	@param {any} previousValue	The last value that was set for this name/node pair
 *	@private
 */
export function setAccessor(node, name, value, old, isSvg) {
	ensureNodeData(node)[name] = value;

	if (name==='key' || name==='children' || name==='innerHTML') return;

	if (name==='class' && !isSvg) {
		node.className = value || '';
	}
	else if (name==='style') {
		if (!value || isString(value) || isString(old)) {
			node.style.cssText = value || '';
		}
		if (value && typeof value==='object') {
			if (!isString(old)) {
				for (let i in old) if (!(i in value)) node.style[i] = '';
			}
			for (let i in value) {
				node.style[i] = typeof value[i]==='number' && !NON_DIMENSION_PROPS[i] ? (value[i]+'px') : value[i];
			}
		}
	}
	else if (name==='dangerouslySetInnerHTML') {
		if (value) node.innerHTML = value.__html;
	}
	else if (name[0]=='o' && name[1]=='n') {
		let l = node._listeners || (node._listeners = {});
		name = toLowerCase(name.substring(2));
		if (value) {
			if (!l[name]) node.addEventListener(name, eventProxy);
		}
		else if (l[name]) {
			node.removeEventListener(name, eventProxy);
		}
		l[name] = value;
	}
	else if (name!=='type' && !isSvg && name in node) {
		setProperty(node, name, empty(value) ? '' : value);
		if (falsey(value)) node.removeAttribute(name);
	}
	else {
		let ns = isSvg && name.match(/^xlink\:?(.+)/);
		if (falsey(value)) {
			if (ns) node.removeAttributeNS('http://www.w3.org/1999/xlink', toLowerCase(ns[1]));
			else node.removeAttribute(name);
		}
		else if (typeof value!=='object' && !isFunction(value)) {
			if (ns) node.setAttributeNS('http://www.w3.org/1999/xlink', toLowerCase(ns[1]), value);
			else node.setAttribute(name, value);
		}
	}
}

/** Attempt to set a DOM property to the given value.
 *	IE & FF throw for certain property-value combinations.
 */
function setProperty(node, name, value) {
	try {
		node[name] = value;
	} catch (e) { }
}

/** Proxy an event to hooked event handlers
 *	@private
 */
function eventProxy(e) {
	return this._listeners[e.type](options.event && options.event(e) || e);
}

/** Get a node's attributes as a hashmap.
 *	@private
 */
export function getRawNodeAttributes(node) {
	let attrs = {};
	for (let i=node.attributes.length; i--; ) {
		attrs[node.attributes[i].name] = node.attributes[i].value;
	}
	return attrs;
}


/* eslint no-console:0 */

/** Find constants (identified by ALL_CAPS_DECLARATIONS), and inline them globally.
 *	This is safe because Preact *only* uses global constants.
 */
export default (file, api) => {
	let j = api.jscodeshift,
		code = j(file.source),
		constants = {},
		found = 0;

	code.find(j.VariableDeclaration)
		.filter( decl => {
			for (let i=decl.value.declarations.length; i--; ) {
				let node = decl.value.declarations[i],
					name = node.id && node.id.name,
					init = node.init;
				if (name && init && name.match(/^[A-Z0-9_$]+$/g)) {
					if (init.type==='Literal') {
						console.log(`Inlining constant: ${name}=${init.raw}`);
						found++;
						constants[name] = init;
						// remove declaration
						decl.value.declarations.splice(i, 1);
						// if it's the last, we'll remove the whole statement
						return !decl.value.declarations.length;
					}
				}
			}
			return false;
		})
		.remove();

	code.find(j.Identifier)
		.filter( path => path.value.name && constants.hasOwnProperty(path.value.name) )
		.replaceWith( path => (found++, constants[path.value.name]) );

	return found ? code.toSource({ quote: 'single' }) : null;
};


import { FORCE_RENDER } from './constants';
import { extend, clone, isFunction } from './util';
import { createLinkedState } from './linked-state';
import { triggerComponentRender, renderComponent } from './vdom/component';

/** Base Component class, for he ES6 Class method of creating Components
 *	@public
 *
 *	@example
 *	class MyFoo extends Component {
 *		render(props, state) {
 *			return <div />;
 *		}
 *	}
 */
export function Component(props, context) {
	/** @private */
	this._dirty = true;
	/** @public */
	this._disableRendering = false;
	/** @public */
	this.prevState = this.prevProps = this.prevContext = this.base = this.nextBase = this._parentComponent = this._component = this.__ref = this.__key = this._linkedStates = this._renderCallbacks = null;
	/** @public */
	this.context = context;
	/** @type {object} */
	this.props = props;
	/** @type {object} */
	this.state = this.getInitialState && this.getInitialState() || {};
}

extend(Component.prototype, {

	/** Returns a `boolean` value indicating if the component should re-render when receiving the given `props` and `state`.
	 *	@param {object} nextProps
	 *	@param {object} nextState
	 *	@param {object} nextContext
	 *	@returns {Boolean} should the component re-render
	 *	@name shouldComponentUpdate
	 *	@function
	 */
	// shouldComponentUpdate() {
	// 	return true;
	// },

	/** Returns a function that sets a state property when called.
	 *	Calling linkState() repeatedly with the same arguments returns a cached link function.
	 *
	 *	Provides some built-in special cases:
	 *		- Checkboxes and radio buttons link their boolean `checked` value
	 *		- Inputs automatically link their `value` property
	 *		- Event paths fall back to any associated Component if not found on an element
	 *		- If linked value is a function, will invoke it and use the result
	 *
	 *	@param {string} key				The path to set - can be a dot-notated deep key
	 *	@param {string} [eventPath]		If set, attempts to find the new state value at a given dot-notated path within the object passed to the linkedState setter.
	 *	@returns {function} linkStateSetter(e)
	 *
	 *	@example Update a "text" state value when an input changes:
	 *		<input onChange={ this.linkState('text') } />
	 *
	 *	@example Set a deep state value on click
	 *		<button onClick={ this.linkState('touch.coords', 'touches.0') }>Tap</button
	 */
	linkState(key, eventPath) {
		let c = this._linkedStates || (this._linkedStates = {}),
			cacheKey = key + '|' + eventPath;
		return c[cacheKey] || (c[cacheKey] = createLinkedState(this, key, eventPath));
	},

	/** Update component state by copying properties from `state` to `this.state`.
	 *	@param {object} state		A hash of state properties to update with new values
	 */
	setState(state, callback) {
		let s = this.state;
		if (!this.prevState) this.prevState = clone(s);
		extend(s, isFunction(state) ? state(s, this.props) : state);
		if (callback) (this._renderCallbacks = (this._renderCallbacks || [])).push(callback);
		triggerComponentRender(this);
	},

	/** Immediately perform a synchronous re-render of the component.
	 *	@private
	 */
	forceUpdate() {
		renderComponent(this, FORCE_RENDER);
	},

	/** Accepts `props` and `state`, and returns a new Virtual DOM tree to build.
	 *	Virtual DOM is generally constructed via [JSX](http://jasonformat.com/wtf-is-jsx).
	 *	@param {object} props		Props (eg: JSX attributes) received from parent element/component
	 *	@param {object} state		The component's current state
	 *	@param {object} context		Context object (if a parent component has provided context)
	 *	@returns VNode
	 */
	render() {
		return null;
	}

});


/*eslint no-var:0, object-shorthand:0 */

var coverage = String(process.env.COVERAGE)!=='false',
	sauceLabs = String(process.env.SAUCELABS).match(/^(1|true)$/gi) && String(process.env.TRAVIS_PULL_REQUEST)!=='true',
	performance = !coverage && !sauceLabs && String(process.env.PERFORMANCE)!=='false',
	webpack = require('webpack');

var sauceLabsLaunchers = {
	sl_chrome: {
		base: 'SauceLabs',
		browserName: 'chrome'
	},
	sl_firefox: {
		base: 'SauceLabs',
		browserName: 'firefox'
	},
	sl_ios_safari: {
		base: 'SauceLabs',
		browserName: 'iphone',
		platform: 'OS X 10.9',
		version: '7.1'
	},
	sl_ie_11: {
		base: 'SauceLabs',
		browserName: 'internet explorer',
		version: '11'
	},
	sl_ie_10: {
		base: 'SauceLabs',
		browserName: 'internet explorer',
		version: '10'
	},
	sl_ie_9: {
		base: 'SauceLabs',
		browserName: 'internet explorer',
		version: '9'
	}
};

module.exports = function(config) {
	config.set({
		browsers: sauceLabs ? Object.keys(sauceLabsLaunchers) : ['PhantomJS'],

		frameworks: ['source-map-support', 'mocha', 'chai-sinon'],

		reporters: ['mocha'].concat(
			coverage ? 'coverage' : [],
			sauceLabs ? 'saucelabs' : []
		),

		coverageReporter: {
			reporters: [
				{
					type: 'text-summary'
				},
				{
					type: 'html',
					dir: __dirname+'/../coverage'
				}
			]
		},

		mochaReporter: {
			showDiff: true
		},

		browserLogOptions: { terminal: true },
		browserConsoleLogOptions: { terminal: true },

		browserNoActivityTimeout: 5 * 60 * 1000,

		// sauceLabs: {
		// 	tunnelIdentifier: process.env.TRAVIS_JOB_NUMBER || ('local'+require('./package.json').version),
		// 	startConnect: false
		// },

		customLaunchers: sauceLabsLaunchers,

		files: [
			{ pattern: '{browser,shared}/**.js', watched: false }
		],

		preprocessors: {
			'**/*': ['webpack', 'sourcemap']
		},

		webpack: {
			devtool: 'inline-source-map',
			module: {
				/* Transpile source and test files */
				preLoaders: [
					{
						test: /\.jsx?$/,
						exclude: /node_modules/,
						loader: 'babel',
						query: {
							loose: 'all',
							blacklist: ['es6.tailCall']
						}
					}
				],
				/* Only Instrument our source files for coverage */
				loaders: [].concat( coverage ? {
					test: /\.jsx?$/,
					loader: 'isparta',
					include: /src/
				} : [])
			},
			resolve: {
				modulesDirectories: [__dirname, 'node_modules']
			},
			plugins: [
				new webpack.DefinePlugin({
					coverage: coverage,
					ENABLE_PERFORMANCE: performance,
					DISABLE_FLAKEY: !!String(process.env.FLAKEY).match(/^(0|false)$/gi)
				})
			]
		},

		webpackMiddleware: {
			noInfo: true
		}
	});
};


import { ATTR_KEY, NON_DIMENSION_PROPS } from '../constants';

import { toLowerCase, empty, falsey, isString, isFunction } from '../util';

import { ATTR_KEY, NON_DIMENSION_PROPS, NON_BUBBLING_EVENTS } from '../constants';


/** Retains a pool of Components for re-use, keyed on component name.
 *	Note: since component names are not unique or even necessarily available, these are primarily a form of sharding.
 *	@private
 */
const components = {};

export function collectComponent(component) {
	let name = component.constructor.name,
		list = components[name];
	if (list) list.push(component);
	else components[name] = [component];
}

export function createComponent(Ctor, props, context) {
	let inst = new Ctor(props, context),
		list = components[Ctor.name];
	inst.props = props;
	inst.context = context;
	if (list) {
		for (let i=list.length; i--; ) {
			if (list[i].constructor===Ctor) {
				inst.nextBase = list[i].nextBase;
				list.splice(i, 1);
				break;
			}
		}
	}
	return inst;
}


The MIT License (MIT)

Copyright (c) 2016 Jason Miller

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.


if (value) node.innerHTML = value.__html;

let l = node._listeners || (node._listeners = {});


import { h, render, Component } from '../../src/preact';
/** @jsx h */

describe('context', () => {
	let scratch;

	before( () => {
		scratch = document.createElement('div');
		(document.body || document.documentElement).appendChild(scratch);
	});

	beforeEach( () => {
		scratch.innerHTML = '';
	});

	after( () => {
		scratch.parentNode.removeChild(scratch);
		scratch = null;
	});

	it('should pass context to grandchildren', () => {
		const CONTEXT = { a:'a' };
		const PROPS = { b:'b' };
		// let inner;

		class Outer extends Component {
			getChildContext() {
				return CONTEXT;
			}
			render(props) {
				return
<Inner {...props} />
;
			}
		}
		sinon.spy(Outer.prototype, 'getChildContext');

		class Inner extends Component {
			// constructor() {
			// 	super();
			// 	inner = this;
			// }
			shouldComponentUpdate() { return true; }
			componentWillReceiveProps() {}
			componentWillUpdate() {}
			componentDidUpdate() {}
			render(props, state, context) {
				return
{ context && context.a }
;
			}
		}
		sinon.spy(Inner.prototype, 'shouldComponentUpdate');
		sinon.spy(Inner.prototype, 'componentWillReceiveProps');
		sinon.spy(Inner.prototype, 'componentWillUpdate');
		sinon.spy(Inner.prototype, 'componentDidUpdate');
		sinon.spy(Inner.prototype, 'render');

		render(<Outer />, scratch, scratch.lastChild);

		expect(Outer.prototype.getChildContext).to.have.been.calledOnce;

		// initial render does not invoke anything but render():
		expect(Inner.prototype.render).to.have.been.calledWith({}, {}, CONTEXT);

		CONTEXT.foo = 'bar';
		render(<Outer {...PROPS} />, scratch, scratch.lastChild);

		expect(Outer.prototype.getChildContext).to.have.been.calledTwice;

		expect(Inner.prototype.shouldComponentUpdate).to.have.been.calledOnce.and.calledWith(PROPS, {}, CONTEXT);
		expect(Inner.prototype.componentWillReceiveProps).to.have.been.calledWith(PROPS, CONTEXT);
		expect(Inner.prototype.componentWillUpdate).to.have.been.calledWith(PROPS, {});
		expect(Inner.prototype.componentDidUpdate).to.have.been.calledWith({}, {});
		expect(Inner.prototype.render).to.have.been.calledWith(PROPS, {}, CONTEXT);

		/* Future:
		 *  Newly created context objects are *not* currently cloned.
		 *  This test checks that they *are* cloned.
		 */
		// Inner.prototype.render.reset();
		// CONTEXT.foo = 'baz';
		// inner.forceUpdate();
		// expect(Inner.prototype.render).to.have.been.calledWith(PROPS, {}, { a:'a', foo:'bar' });
	});

	it('should pass context to direct children', () => {
		const CONTEXT = { a:'a' };
		const PROPS = { b:'b' };

		class Outer extends Component {
			getChildContext() {
				return CONTEXT;
			}
			render(props) {
				return <Inner {...props} />;
			}
		}
		sinon.spy(Outer.prototype, 'getChildContext');

		class Inner extends Component {
			shouldComponentUpdate() { return true; }
			componentWillReceiveProps() {}
			componentWillUpdate() {}
			componentDidUpdate() {}
			render(props, state, context) {
				return
{ context && context.a }
;
			}
		}
		sinon.spy(Inner.prototype, 'shouldComponentUpdate');
		sinon.spy(Inner.prototype, 'componentWillReceiveProps');
		sinon.spy(Inner.prototype, 'componentWillUpdate');
		sinon.spy(Inner.prototype, 'componentDidUpdate');
		sinon.spy(Inner.prototype, 'render');

		render(<Outer />, scratch, scratch.lastChild);

		expect(Outer.prototype.getChildContext).to.have.been.calledOnce;

		// initial render does not invoke anything but render():
		expect(Inner.prototype.render).to.have.been.calledWith({}, {}, CONTEXT);

		CONTEXT.foo = 'bar';
		render(<Outer {...PROPS} />, scratch, scratch.lastChild);

		expect(Outer.prototype.getChildContext).to.have.been.calledTwice;

		expect(Inner.prototype.shouldComponentUpdate).to.have.been.calledOnce.and.calledWith(PROPS, {}, CONTEXT);
		expect(Inner.prototype.componentWillReceiveProps).to.have.been.calledWith(PROPS, CONTEXT);
		expect(Inner.prototype.componentWillUpdate).to.have.been.calledWith(PROPS, {});
		expect(Inner.prototype.componentDidUpdate).to.have.been.calledWith({}, {});
		expect(Inner.prototype.render).to.have.been.calledWith(PROPS, {}, CONTEXT);

		// make sure render() could make use of context.a
		expect(Inner.prototype.render).to.have.returned(sinon.match({ children:['a'] }));
	});

	it('should preserve existing context properties when creating child contexts', () => {
		let outerContext = { outer:true },
			innerContext = { inner:true };
		class Outer extends Component {
			getChildContext() {
				return { outerContext };
			}
			render() {
				return
<Inner />
;
			}
		}

		class Inner extends Component {
			getChildContext() {
				return { innerContext };
			}
			render() {
				return <InnerMost />;
			}
		}

		class InnerMost extends Component {
			render() {
				return test;
			}
		}

		sinon.spy(Inner.prototype, 'render');
		sinon.spy(InnerMost.prototype, 'render');

		render(<Outer />, scratch);

		expect(Inner.prototype.render).to.have.been.calledWith({}, {}, { outerContext });
		expect(InnerMost.prototype.render).to.have.been.calledWith({}, {}, { outerContext, innerContext });
	});
});


import { ATTR_KEY } from '../constants';
import { toLowerCase, empty, isString, isFunction } from '../util';
import { isSameNodeType, isNamedNode } from './index';
import { isFunctionalComponent, buildFunctionalComponent } from './functional-component';
import { buildComponentFromVNode } from './component';
import { setAccessor, getRawNodeAttributes, getNodeType } from '../dom/index';
import { createNode, collectNode } from '../dom/recycler';
import { unmountComponent } from './component';

/** Diff recursion count, used to track the end of the diff cycle. */
export const mounts = [];

/** Diff recursion count, used to track the end of the diff cycle. */
export let diffLevel = 0;

let isSvgMode = false;

export function flushMounts() {
	let c;
	while ((c=mounts.pop())) {
		if (c.componentDidMount) c.componentDidMount();
	}
}

/** Apply differences in a given vnode (and it's deep children) to a real DOM Node.
 *	@param {Element} [dom=null]		A DOM node to mutate into the shape of the `vnode`
 *	@param {VNode} vnode			A VNode (with descendants forming a tree) representing the desired DOM structure
 *	@returns {Element} dom			The created/mutated element
 *	@private
 */
export function diff(dom, vnode, context, mountAll, parent, rootComponent, nextSibling) {
	diffLevel++;
	let ret = idiff(dom, vnode, context, mountAll, rootComponent);
	if (parent && ret.parentNode!==parent) {
		parent.insertBefore(ret, nextSibling || null);
	}
	if (!--diffLevel) flushMounts();
	return ret;
}

function idiff(dom, vnode, context, mountAll, rootComponent) {
	let originalAttributes = vnode && vnode.attributes;

	while (isFunctionalComponent(vnode)) {
		vnode = buildFunctionalComponent(vnode, context);
	}

	if (empty(vnode)) {
		vnode = '';
		if (rootComponent) {
			if (dom) {
				if (dom.nodeType===8) return dom;
				collectNode(dom);
			}
			return document.createComment(vnode);
		}
	}

	if (isString(vnode)) {
		if (dom) {
			if (getNodeType(dom)===3 && dom.parentNode) {
				dom.nodeValue = vnode;
				return dom;
			}
			collectNode(dom);
		}
		return document.createTextNode(vnode);
	}

	let out = dom,
		nodeName = vnode.nodeName,
		svgMode;

	if (isFunction(nodeName)) {
		return buildComponentFromVNode(dom, vnode, context, mountAll);
	}

	if (!isString(nodeName)) {
		nodeName = String(nodeName);
	}

	svgMode = toLowerCase(nodeName)==='svg';

	if (svgMode) isSvgMode = true;

	if (!dom) {
		out = createNode(nodeName, isSvgMode);
	}
	else if (!isNamedNode(dom, nodeName)) {
		out = createNode(nodeName, isSvgMode);
		// move children into the replacement node
		while (dom.firstChild) out.appendChild(dom.firstChild);
		// reclaim element nodes
		recollectNodeTree(dom);
	}

	// fast-path for elements containing a single TextNode:
	if (vnode.children && vnode.children.length===1 && typeof vnode.children[0]==='string' && out.childNodes.length===1 && out.firstChild instanceof Text) {
		out.firstChild.nodeValue = vnode.children[0];
	}
	else if (vnode.children || out.firstChild) {
		innerDiffNode(out, vnode.children, context, mountAll);
	}

	diffAttributes(out, vnode.attributes);

	if (originalAttributes && originalAttributes.ref) {
		(out[ATTR_KEY].ref = originalAttributes.ref)(out);
	}

	if (svgMode) isSvgMode = false;

	return out;
}

/** Apply child and attribute changes between a VNode and a DOM Node to the DOM. */
function innerDiffNode(dom, vchildren, context, mountAll) {
	let originalChildren = dom.childNodes,
		children = [],
		keyed = {},
		keyedLen = 0,
		min = 0,
		len = originalChildren.length,
		childrenLen = 0,
		vlen = vchildren && vchildren.length,
		j, c, vchild, child;

	if (len) {
		for (let i=0; i<len; i++) {
			let child = originalChildren[i],
				key = vlen ? ((c = child._component) ? c.__key : (c = child[ATTR_KEY]) ? c.key : null) : null;
			if (key || key===0) {
				keyedLen++;
				keyed[key] = child;
			}
			else {
				children[childrenLen++] = child;
			}
		}
	}

	if (vlen) {
		for (let i=0; i<vlen; i++) {
			vchild = vchildren[i];
			child = null;

			// if (isFunctionalComponent(vchild)) {
			// 	vchild = buildFunctionalComponent(vchild);
			// }

			// attempt to find a node based on key matching
			if (keyedLen && vchild.attributes) {
				let key = vchild.key;
				if (!empty(key) && key in keyed) {
					child = keyed[key];
					keyed[key] = undefined;
					keyedLen--;
				}
			}

			// attempt to pluck a node of the same type from the existing children
			if (!child && min<childrenLen) {
				for (j=min; j<childrenLen; j++) {
					c = children[j];
					if (c && isSameNodeType(c, vchild)) {
						child = c;
						children[j] = undefined;
						if (j===childrenLen-1) childrenLen--;
						if (j===min) min++;
						break;
					}
				}
			}

			// morph the matched/found/created DOM child to match vchild (deep)
			child = idiff(child, vchild, context, mountAll);

			if (child!==originalChildren[i]) {
				dom.insertBefore(child, originalChildren[i] || null);
			}
		}
	}

	if (keyedLen) {
		/*eslint guard-for-in:0*/
		for (let i in keyed) if (keyed[i]) {
			children[min=childrenLen++] = keyed[i];
		}
	}

	// remove orphaned children
	if (min<childrenLen) {
		removeOrphanedChildren(children);
	}
}

/** Reclaim children that were unreferenced in the desired VTree */
export function removeOrphanedChildren(children, unmountOnly) {
	for (let i=children.length; i--; ) {
		let child = children[i];
		if (child) {
			recollectNodeTree(child, unmountOnly);
		}
	}
}

/** Reclaim an entire tree of nodes, starting at the root. */
export function recollectNodeTree(node, unmountOnly) {
	// @TODO: Need to make a call on whether Preact should remove nodes not created by itself.
	// Currently it *does* remove them. Discussion: https://github.com/developit/preact/issues/39
	//if (!node[ATTR_KEY]) return;

	let component = node._component;
	if (component) {
		unmountComponent(component, !unmountOnly);
	}
	else {
		if (node[ATTR_KEY] && node[ATTR_KEY].ref) node[ATTR_KEY].ref(null);

		if (!unmountOnly) {

			collectNode(node);
		}

		if (node.childNodes && node.childNodes.length) {
			removeOrphanedChildren(node.childNodes, unmountOnly);
		}
	}
}

/** Apply differences in attributes from a VNode to the given DOM Node. */
function diffAttributes(dom, attrs) {
	let old = dom[ATTR_KEY] || getRawNodeAttributes(dom);

	// removeAttributes(dom, old, attrs || EMPTY);
	for (let name in old) {
		if (!attrs || !(name in attrs)) {
			setAccessor(dom, name, null, old[name], isSvgMode);
		}
	}

	// new & updated
	if (attrs) {
		for (let name in attrs) {
			if (!(name in old) || attrs[name]!=old[name] || ((name==='value' || name==='checked') && attrs[name]!=dom[name])) {
				setAccessor(dom, name, attrs[name], old[name], isSvgMode);
			}
		}
	}
}


if (!l[name]) node.addEventListener(name, eventProxy);

if (!l[name]) node.addEventListener(name, eventProxy, capture);


import { diff } from './vdom/diff';

/** Render JSX into a `parent` Element.
 *	@param {VNode} vnode		A (JSX) VNode to render
 *	@param {Element} parent		DOM element to render into
 *	@param {Element} [merge]	Attempt to re-use an existing DOM tree rooted at `merge`
 *	@public
 *
 *	@example
 *	// render a div into :
 *	render(
hello!
, document.body);
 *
 *	@example
 *	// render a "Thing" component into #foo:
 *	const Thing = ({ name }) => { name };
 *	render(<Thing name="one" />, document.querySelector('#foo'));
 */
export function render(vnode, parent, merge) {
	return diff(merge, vnode, {}, false, parent);
}


import { empty, isString, isFunction, delve } from './util';

/** Create an Event handler function that sets a given state property.
 *	@param {Component} component	The component whose state should be updated
 *	@param {string} key				A dot-notated key path to update in the component's state
 *	@param {string} eventPath		A dot-notated key path to the value that should be retrieved from the Event or component
 *	@returns {function} linkedStateHandler
 *	@private
 */
export function createLinkedState(component, key, eventPath) {
	let path = key.split('.'),
		p0 = path[0];
	return function(e) {
		let t = e && e.currentTarget || this,
			s = component.state,
			obj = s,
			v, i;
		if (isString(eventPath)) {
			v = delve(e, eventPath);
			if (empty(v) && (t=t._component)) {
				v = delve(t, eventPath);
			}
		}
		else {
			v = t.nodeName ? ((t.nodeName+t.type).match(/^input(check|rad)/i) ? t.checked : t.value) : e;
		}
		if (isFunction(v)) v = v.call(t);
		if (path.length>1) {
			for (i=0; i<path.length-1; i++) {
				obj = obj[path[i]] || (obj[path[i]] = {});
			}
			obj[path[i]] = v;
			v = s[p0];
		}
		component.setState({ [p0]: v });
	};
}