adobe/brackets

View on GitHub
src/extensions/default/CodeFolding/unittests.js

Summary

Maintainability
C
1 day
Test Coverage
/**
 * Codefolding unit test files
 * @author Patrick Oladimeji
 * @date 01/08/2015 18:34
 */

/*global describe, beforeEach, afterEach, it, expect, runs, waitsForDone, waitsFor*/

define(function (require, exports, module) {
    "use strict";
    var SpecRunnerUtils = brackets.getModule("spec/SpecRunnerUtils"),
        FileUtils = brackets.getModule("file/FileUtils");

    describe("Code Folding", function () {
        var testWindow,
            testEditor,
            EditorManager,
            DocumentManager,
            CommandManager,
            PreferencesManager,
            prefs,
            cm,
            gutterName = "CodeMirror-foldgutter",
            foldMarkerOpen = gutterName + "-open",
            foldMarkerClosed = gutterName + "-folded";
        var extensionPath = FileUtils.getNativeModuleDirectoryPath(module),
            testDocumentDirectory = extensionPath + "/unittest-files/",
            // The line numbers referenced below are dependent on the files in /unittest-files directory.
            // Remember to update the numbers if the files change.
            testFilesSpec = {
                js: {
                    filePath: testDocumentDirectory + "test.js",
                    foldableLines: [1, 11, 17, 21, 25, 27, 30],
                    sameLevelFoldableLines: [17, 21],
                    firstSelection: {start: {line: 2, ch: 0}, end: {line: 10, ch: 0}},
                    secondSelection: {start: {line: 5, ch: 0}, end: {line: 8, ch: 4}}
                },
                html: {
                    filePath: testDocumentDirectory + "test.html",
                    foldableLines: [1, 2, 3, 4, 8, 9, 14, 15, 16, 20, 21, 22, 23, 28, 29, 34, 37],
                    sameLevelFoldableLines: [3, 8],
                    firstSelection: {start: {line: 38, ch: 0}, end: {line: 41, ch: 0}},
                    secondSelection: {start: {line: 42, ch: 0}, end: {line: 45, ch: 4}}
                },
                hbs: {
                    filePath: testDocumentDirectory + "test.hbs",
                    foldableLines: [1, 7, 14, 16, 17, 21, 26, 28, 29, 32, 33, 38, 41],
                    sameLevelFoldableLines: [1, 7, 14],
                    firstSelection: {start: {line: 2, ch: 0}, end: {line: 10, ch: 0}},
                    secondSelection: {start: {line: 5, ch: 0}, end: {line: 8, ch: 4}}
                }
            },
            open = "open",
            folded = "folded";

        /**
         * Utility to temporarily set preference values in the session scope
         */
        function setPreference(key, value) {
            prefs.set(key, value, {
                locations: {
                    scope: "session"
                }
            });
        }

        /**
         * Open a test file
         * @param {String} path The path to the file to open
         */
        function openTestFile(path) {
            var promise = SpecRunnerUtils.openProjectFiles([path]);
            promise.then(function () {
                testEditor = EditorManager.getCurrentFullEditor();
                cm = testEditor._codeMirror;
            });
            waitsForDone(promise, "Test file opened", 3000);
        }

        /**
         * Sets up the test window and loads the test project
         */
        function setup() {
            runs(function () {
                SpecRunnerUtils.createTestWindowAndRun(this, function (w) {
                    testWindow = w;
                    EditorManager = w.brackets.test.EditorManager;
                    DocumentManager = w.brackets.test.DocumentManager;
                    PreferencesManager = w.brackets.test.PreferencesManager;
                    CommandManager = w.brackets.test.CommandManager;

                    prefs = PreferencesManager.getExtensionPrefs("code-folding");
                }, {hasNativeMenus: true});
            });

            runs(function () {
                //setPreference("saveFoldStates", false);
                SpecRunnerUtils.loadProjectInTestWindow(testDocumentDirectory);
            });
        }

        /**
         * Closes the test window
         */
        function tearDown() {
            SpecRunnerUtils.closeTestWindow();
        }

        /**
         * Runs the specified command
         * @param   {String}  command The command to run
         * @returns {Promise} A promise that resolves after command execution is completed or failed
         */
        function runCommand(command) {
            return CommandManager.execute(command);
        }

        /**
         * Folds the code on the given line number
         * @param {Number} line The line number to fold
         */
        function foldCodeOnLine(line) {
            cm.setCursor(line - 1);
            var promise = runCommand("codefolding.collapse");
            waitsForDone(promise, "Collapse code", 2000);
        }

        /**
         * Expands the code on the given line number
         * @param {Number} line The line number to fold
         */
        function expandCodeOnLine(line) {
            cm.setCursor(line - 1);
            var promise = runCommand("codefolding.expand");
            waitsForDone(promise, "Expand code", 2000);
        }

        /**
         * Returns all the fold marks currently inside the editor
         * @returns {Array<TextMark>} The fold marks currently inside the editor
         */
        function getEditorFoldMarks() {
            testEditor = EditorManager.getCurrentFullEditor();
            cm = testEditor._codeMirror;

            var marks = cm.getAllMarks().filter(function (m) {
                return m.__isFold;
            });
            return marks;
        }

        /**
         * Gets information about the mark in the gutter specifically whether it is folded or open.
         * @param   {Object} lineInfo The CodeMirror lineInfo object
         * @returns {Object} an object with line and type property
         */
        function gutterMarkState(lineInfo) {
            if (!lineInfo || !lineInfo.gutterMarkers) {
                return;
            }
            var classes = lineInfo.gutterMarkers[gutterName].classList;
            if (classes && classes.contains(foldMarkerClosed)) {
                return {line: lineInfo.line, type: folded};
            } else if (classes && classes.contains(foldMarkerOpen)) {
                return {line: lineInfo.line, type: open};
            }
            return;
        }

        /**
         * Helper function to return the fold markers on the current codeMirror instance
         *
         * @returns {Array<object>} An array of objects containing the line and the type of marker.
         */
        function getGutterFoldMarks() {
            testEditor = EditorManager.getCurrentFullEditor();
            cm = testEditor._codeMirror;
            var marks = [];
            cm.eachLine(function (lineHandle) {
                var lineInfo = cm.lineInfo(lineHandle);
                marks.push(gutterMarkState(lineInfo));
            });

            return marks.filter(function (m) { return m; });
        }

        /**
         * Helper function to filter out all open gutter markers
         * @param   {Object}  m the marker to filter
         * @returns {boolean} true if the marker is open or false otherwise
         */
        function filterOpen(m) {
            return m.type === open;
        }

        /**
         * Helper function to filter out all closed gutter markers.
         * @param   {Object}  m the marker to to filter
         * @returns {boolean} true if the marker is closed or false otherwise
         */
        function filterFolded(m) {
            return m.type === folded;
        }

        /*
         * Helper function to return the line number on a marker
         * @param   {Object} m the maker whose line number we want to retrieve
         * @returns {Number} the line number of the marker
         */
        function getLineNumber(m) {
            return m.line;
        }

        /**
         * Helper function to change the lines to zero-based index
         * @param   {Array<number>} lines the line numbers to change to zero base index
         * @returns {Array<number>} the zero-based index of the lines passed in
         */
        function toZeroIndex(lines) {
            return lines.map(function (l) {
                return l - 1;
            });
        }

        /**
         * Helper function to select a range of text in the editor
         * @param   {CodeMirror.Pos} start the start position of the selection
         * @param   {CodeMirror.Pos} end   the end position of the selection
         */
        function selectTextInEditor(start, end) {
            var marksVisible = false;
            runs(function () {
                cm.setSelection(start, end);
                setTimeout(function () {
                    //wait for foldmarks to be rendered
                    marksVisible = true;
                }, 410);
            });

            waitsFor(function () {
                return marksVisible;
            }, "Fold markers now visible in gutter", 500);
        }

        beforeEach(function () {
            setup();
        });

        afterEach(function () {
            testWindow.closeAllFiles();
            tearDown();
        });

        Object.keys(testFilesSpec).forEach(function (file) {
            var testFilePath = testFilesSpec[file].filePath;
            var foldableLines = testFilesSpec[file].foldableLines;
            var testFileSpec = testFilesSpec[file];
            describe(file + " - Editor/Gutter", function () {
                beforeEach(function () {
                    runs(function () {
                        openTestFile(testFilePath);
                    });

                    runs(function () {
                        testEditor = EditorManager.getCurrentFullEditor();
                        cm = testEditor._codeMirror;
                    });
                });

                afterEach(function () {
                    testWindow.closeAllFiles();
                });

                it("renders fold marks on startup", function () {
                    var marks = getGutterFoldMarks();
                    expect(marks.length).toBeGreaterThan(0);
                    marks.map(getLineNumber).forEach(function (line) {
                        expect(toZeroIndex(foldableLines)).toContain(line);
                    });
                });

                it("creates a folded region in editor when fold marker is clicked", function () {
                    var lineNumber = foldableLines[0];
                    runs(function () {
                        foldCodeOnLine(lineNumber);
                    });

                    runs(function () {
                        var marks = getEditorFoldMarks();
                        expect(marks.length).toEqual(1);
                        expect(marks[0].lines[0].lineNo()).toEqual(lineNumber - 1);
                    });
                });

                it("clears the folded region in editor when collapsed fold marker is clicked", function () {
                    var lineNumber = foldableLines[0];
                    runs(function () {
                        foldCodeOnLine(lineNumber);
                    });
                    runs(function () {
                        expandCodeOnLine(lineNumber);
                    });

                    runs(function () {
                        var marks = getEditorFoldMarks();
                        expect(marks.length).toEqual(0);
                    });
                });

                it("expands and updates the fold gutter when text marker for a folded region in editor is cleared", function () {
                    var lineNumber = foldableLines[0];
                    runs(function () {
                        foldCodeOnLine(lineNumber);
                    });
                    runs(function () {
                        var marks = getEditorFoldMarks().filter(function (m) {
                            var range = m.find();
                            return range ? range.from.line === lineNumber - 1 : false;
                        });
                        marks[0].clear();
                    });

                    runs(function () {
                        var marks = getEditorFoldMarks();
                        var gutterMark = getGutterFoldMarks().filter(function (m) {
                            return m.line === lineNumber - 1 && m.type === open;
                        });
                        expect(marks.length).toEqual(0);
                        expect(gutterMark.length).toEqual(1);

                    });
                });

                it("renders folded marker in the gutter for folded code regions", function () {
                    var lineNumbers = testFilesSpec[file].sameLevelFoldableLines;
                    runs(function () {
                        lineNumbers.forEach(function (l) {
                            foldCodeOnLine(l);
                        });
                    });

                    runs(function () {
                        var marks = getGutterFoldMarks().filter(filterFolded);
                        expect(marks.length).toEqual(lineNumbers.length);

                        var gutterNumbers = marks
                            .map(getLineNumber);
                        expect(gutterNumbers).toEqual(toZeroIndex(lineNumbers));
                    });
                });

                it("indicates foldable lines in the gutter", function () {
                    var lineNumbers = foldableLines;
                    var marks = getGutterFoldMarks();
                    var gutterNumbers = marks.filter(filterOpen)
                        .map(getLineNumber);
                    expect(gutterNumbers).toEqual(toZeroIndex(lineNumbers));
                });

                describe("Preferences", function () {
                    it("persists fold states", function () {
                        var lineNumbers = testFileSpec.sameLevelFoldableLines;
                        runs(function () {
                            lineNumbers.forEach(function (line) {
                                foldCodeOnLine(line);
                            });
                        });
                        runs(function () {
                            testWindow.closeAllFiles();
                        });

                        runs(function () {
                            openTestFile(testFilePath);
                        });

                        runs(function () {
                            var marks = getEditorFoldMarks();
                            var gutterNumbers = marks.map(function (mark) {
                                return mark.lines[0].lineNo();
                            });
                            expect(gutterNumbers).toEqual(toZeroIndex(lineNumbers));
                        });
                    });

                    it("can disable persistence of fold states", function () {
                        setPreference("saveFoldStates", false);
                        runs(function () {
                            foldCodeOnLine(foldableLines[0]);
                        });
                        runs(function () {
                            testWindow.closeAllFiles();
                        });

                        runs(function () {
                            openTestFile(testFilePath);
                        });

                        runs(function () {
                            var marks = getEditorFoldMarks();
                            expect(marks.length).toEqual(0);
                        });
                        
                        runs(function () {
                            var lineNumbers = foldableLines;
                            var marks = getGutterFoldMarks();
                            var gutterNumbers = marks.filter(filterOpen)
                                .map(getLineNumber);
                            expect(gutterNumbers).toEqual(toZeroIndex(lineNumbers));
                        });
                    });

                    it("can set the minimum fold size", function () {
                        setPreference("minFoldSize", 20000);
                        runs(function () {
                            testWindow.closeAllFiles();
                        });

                        runs(function () {
                            openTestFile(testFilePath);
                        });

                        runs(function () {
                            var marks = getGutterFoldMarks();
                            expect(marks.length).toEqual(0);
                        });
                    });

                    it("can disable code folding", function () {
                        setPreference("enabled", false);
                        runs(function () {
                            var marks = getEditorFoldMarks();
                            expect(marks.length).toEqual(0);
                        });
                    });

                    describe("Fold selected region", function () {
                        it("can be enabled by setting `makeSelectionsFoldable' to true", function () {
                            var start = testFileSpec.firstSelection.start, end = testFileSpec.firstSelection.end;
                            setPreference("makeSelectionsFoldable", true);

                            selectTextInEditor(start, end);

                            runs(function () {
                                var marks = getGutterFoldMarks().filter(filterOpen).map(getLineNumber);
                                expect(marks).toContain(start.line);
                            });
                        });

                        it("can be disabled by setting `makeSelectionsFoldable' to false", function () {
                            setPreference("makeSelectionsFoldable", false);
                            var start = testFileSpec.firstSelection.start, end = testFileSpec.firstSelection.end;
                            selectTextInEditor(start, end);

                            runs(function () {
                                var marks = getGutterFoldMarks().filter(filterOpen)
                                    .map(getLineNumber).filter(function (d) {
                                        return d === start.line;
                                    });
                                expect(marks.length).toEqual(0);
                            });
                        });

                        it("shows fold ranges for only the most recent selection", function () {
                            var firstSelection = testFileSpec.firstSelection,
                                secondSelection = testFileSpec.secondSelection;

                            selectTextInEditor(firstSelection.start, firstSelection.end);

                            selectTextInEditor(secondSelection.start, secondSelection.end);

                            runs(function () {
                                var marks = getGutterFoldMarks().filter(filterOpen)
                                    .map(getLineNumber);
                                expect(marks).toContain(secondSelection.start.line);
                                expect(marks).not.toContain(firstSelection.start.line);
                            });
                        });
                    });

                });

                describe("Editor text changes", function () {
                    var foldableLine = foldableLines[1],
                        expandTimeoutElapsed = false;

                    // add a line after folding a region preserves the region and the region can be unfolded
                    it("can unfold a folded region after a line has been added above it", function () {
                        runs(function () {
                            foldCodeOnLine(foldableLine);
                            cm.replaceRange("\r\n", {line: foldableLine - 1, ch: 0});
                        });

                        runs(function () {
                            expandCodeOnLine(foldableLine + 1);
                            setTimeout(function () {
                                expandTimeoutElapsed = true;
                            }, 400);
                        });

                        waitsFor(function () {
                            return expandTimeoutElapsed;
                        }, "waiting a moment for gutter markerts to be re-rendered", 500);

                        runs(function () {
                            var marks = getGutterFoldMarks().filter(filterFolded);
                            expect(marks.length).toEqual(0);
                        });

                    });

                    it("can unfold a folded region even after a line has been removed above it", function () {
                        runs(function () {
                            foldCodeOnLine(foldableLine);
                            cm.replaceRange("", {line: foldableLine - 1, ch: 0}, {line: foldableLine, ch: 0});
                        });

                        runs(function () {
                            expandCodeOnLine(foldableLine - 1);
                            setTimeout(function () {
                                expandTimeoutElapsed = true;
                            }, 400);
                        });

                        waitsFor(function () {
                            return expandTimeoutElapsed;
                        }, "waiting a moment for gutter markerts to be re-rendered", 500);

                        runs(function () {
                            var marks = getGutterFoldMarks().filter(filterFolded);
                            expect(marks.length).toEqual(0);
                        });
                    });
                });
            });
        });
    });
});