测试一个复杂的AngularJS模块(自动展开菜单)

Testing a complex AngularJS module (auto-expanding menu)

本文关键字:菜单 模块 一个 复杂 测试 AngularJS      更新时间:2023-09-26

我已经用AngularJS构造了一个相当复杂的菜单,我希望能得到一些关于如何测试它的指导。该项目使用了Jasmine和Karma。我在网上找到的所有示例和教程似乎都只演示了非常简单的指令和控制器。

它在Plunker上运行:http://plnkr.co/edit/6bgf5E6oP7v11I4i9SVz?p=preview

我甚至不知道从哪里开始,所以任何帮助都是非常感谢的。

代码:

HTML

<jl-menu data-ng-class="menuState()" jl-scrollable-parent="#wrap" jl-fixed-offset-element="#wrap" jl-fixed-offset-y="{{menuOffset()}}" jl-scroll-offset="0" class="menu_wrap">
  <jl-menu-range jl-from-element="#section1" jl-from-offset="0" jl-from-attr="top" jl-to-element="#section3" jl-to-offset="0" jl-to-attr="top"></jl-menu-range>
  <jl-menu-range jl-from-element="#section4" jl-from-offset="0" jl-from-attr="top" jl-to-element="#section5" jl-to-offset="0" jl-to-attr="top"></jl-menu-range>
  <jl-menu-range jl-from-element="#section6" jl-from-offset="0" jl-from-attr="top" jl-to-element="#section6" jl-to-offset="0" jl-to-attr="bottom"></jl-menu-range>
  <ul class="menu">
    <li data-ng-class="menuItemState('section1')">
      <a href="#section1" jl-scroll-to="section1" jl-scrollable-parent="#wrap">Section 1</a>
      <ul class="submenu" data-ng-class="submenuState('section1')">
        <li data-ng-class="menuItemState('section1_1')">
          <a href="#section1_1" jl-scroll-to="section1_1" jl-scrollable-parent="#wrap">Section1, 1</a>
        </li>
        <li data-ng-class="menuItemState('section1_2')">
          <a href="#section1_2" jl-scroll-to="section1_2" jl-scrollable-parent="#wrap">Section1, 2</a>
        </li>
        <li data-ng-class="menuItemState('section1_3')">
          <a href="#section1_3" jl-scroll-to="section1_3" jl-scrollable-parent="#wrap">Section1, 3</a>
        </li>
      </ul>
    </li>
    <li data-ng-class="menuItemState('section2')">
      <a href="#section2" jl-scroll-to="section2" jl-scrollable-parent="#wrap">Section 2</a>
      <ul class="submenu" data-ng-class="submenuState('section2')">
        <li data-ng-class="menuItemState('section2_1')">
          <a href="#section2_1" jl-scroll-to="section2_1" jl-scrollable-parent="#wrap">Section2, 1</a>
        </li>
        <li data-ng-class="menuItemState('section2_2')">
          <a href="#section2_2" jl-scroll-to="section2_2" jl-scrollable-parent="#wrap">Section2, 2</a>
        </li>
        <li data-ng-class="menuItemState('section2_3')">
          <a href="#section2_3" jl-scroll-to="section2_3" jl-scrollable-parent="#wrap">Section2, 3</a>
        </li>
      </ul>
    </li>
    <li data-ng-class="menuItemState('section3')">
      <a href="#section3" jl-scroll-to="section3" jl-scrollable-parent="#wrap">Section 3</a>
    <li data-ng-class="menuItemState('section4')">
      <a href="#section4" jl-scroll-to="section4" jl-scrollable-parent="#wrap">Section 4</a>
      <ul class="submenu" data-ng-class="submenuState('section4')">
        <li data-ng-class="menuItemState('section4_1')">
          <a href="#section4_1" jl-scroll-to="section4_1" jl-scrollable-parent="#wrap">Section4, 1</a>
        </li>
        <li data-ng-class="menuItemState('section4_2')">
          <a href="#section4_2" jl-scroll-to="section4_2" jl-scrollable-parent="#wrap">Section4, 2</a>
        </li>
        <li data-ng-class="menuItemState('section4_3')">
          <a href="#section4_3" jl-scroll-to="section4_3" jl-scrollable-parent="#wrap">Section4, 3</a>
        </li>
      </ul>
    </li>
    <li data-ng-class="menuItemState('section5')">
      <a href="#section5" jl-scroll-to="section5" jl-scrollable-parent="#wrap">Section 5</a>
    <li data-ng-class="menuItemState('section6')">
      <a href="#section6" jl-scroll-to="section6" jl-scrollable-parent="#wrap">Section 6</a>
      <ul class="submenu" data-ng-class="submenuState('section6')">
        <li data-ng-class="menuItemState('section6_1')">
          <a href="#section6_1" jl-scroll-to="section6_1" jl-scrollable-parent="#wrap">section6, 1</a>
        </li>
        <li data-ng-class="menuItemState('section6_2')">
          <a href="#section6_2" jl-scroll-to="section6_2" jl-scrollable-parent="#wrap">section6, 2</a>
        </li>
        <li data-ng-class="menuItemState('section6_3')">
          <a href="#section6_3" jl-scroll-to="section6_3" jl-scrollable-parent="#wrap">section6, 3</a>
        </li>
      </ul>
    </li>
  </ul>
</jl-menu>
Javascript

angular.module("App", [
  "Common",
  "Menu"])
  .config(["$logProvider", function($logProvider) {
    $logProvider.debugEnabled(true);
  }]);
angular.module("Common", [])
  .config(["$logProvider", function($logProvider) {
    $logProvider.debugEnabled(true);
  }])
  .directive("jlDocumentOffsetY", ["$log", function($log) {
    function link(scope, element, attrs) {
      attrs.$observe("jlDocumentOffsetY", function(value) {
        var y = parseInt(value);
        if (isNaN(y)) {
          $log.error("Error parsing int expression in jlDocumentOffsetY directive");
          return;
        }
        angular.element(element).offset({ top: y });
      });
    }
    return {
      restrict: "A",
      link: link
    };
  }])
  .directive("jlFixedOffsetY", ["$log", "$window", function($log, $window) {
    function link(scope, element, attrs) {
      var container = angular.element($window);
      if (attrs.jlFixedOffsetElement && attrs.jlFixedOffsetElement != "window") {
        container = angular.element(attrs.jlFixedOffsetElement);
        if (container.length === 0) {
          $log.warn("Could not find element '" + attrs.jlFixedOffsetElement + "'");
          container = angular.element($window);
        }
      }
      attrs.$observe("jlFixedOffsetY", function(value) {
        var y = parseInt(value);
        if (isNaN(y)) {
          $log.error("Error parsing int expression in jlFixedOffsetY directive");
          return;
        }
        angular.element(element).offset({ top: y + container.scrollTop() });
      });
    }
    return {
      restrict: "A",
      link: link
    };
  }]);
angular.module("Menu", ["Common"])
  .config(["$logProvider", function($logProvider) {
    $logProvider.debugEnabled(true);
  }])
  .directive("jlMenu", ["$window", function($window) {
    function link(scope, element, attrs) {
      if (attrs.jlScrollOffset) {
        scope.scrollOffset = parseInt(attrs.jlScrollOffset);
      }
      else {
        scope.scrollOffset = 0;
      }
      scope.scrollable = angular.element($window);
      scope.scrollableIsWindow = true;
      if (attrs.jlScrollableParent && attrs.jlScrollableParent != "window") {
        scope.scrollable = angular.element(attrs.jlScrollableParent);
        if (scope.scrollable.length === 0) {
          $log.warn("Could not find element '" + jlScrollableParent + "'");
          scope.scrollable = angular.element($window);
        }
        else {
          scope.scrollableIsWindow = false;
        }
      }
      scope.scrollable.bind("scroll", function() {
        scope.$apply();
      });
    }
    return {
      restrict: "E",
      controller: [
      "$scope", "$element", "$document", "$interval",
      function($scope, $element, $document, $interval) {
        var element = angular.element($element);
        var ranges = [];
        var offsetY;
        var prevRange = -1;
        var range = -1;
        var nextRange = -1;
        var pageH = angular.element($document).height();
        var ST_DOCKED = 0;      // Menu is in fixed position
        var ST_HALT_ABOVE = 1;  // Menu is positioned at the bottom of above range.
        var ST_HALT_BELOW = 2;  // Menu is positioned at top of below range.
        var cssClasses = {
          classes: [],
          add: function(css) {
            if (this.classes.indexOf(css) == -1) {
              this.classes.push(css);
            }
          },
          remove: function(css) {
            var i = this.classes.indexOf(css);
            if (i != -1) {
              this.classes.splice(i, 1);
            }
          },
          get: function() {
            return this.classes.join(" ");
          }
        };
        function getState() {
          var scrollTop = $scope.scrollable.scrollTop();
          var offset = $scope.scrollOffset;
          prevRange = -1;
          range = -1;
          nextRange = -1;
          for (var i = 0; i < ranges.length; ++i) {
            if (scrollTop + offset > ranges[i].from) {
              if (scrollTop + offset < ranges[i].to) {
                range = i;
              }
              else {
                prevRange = i;
              }
            }
            else {
              nextRange = i;
              break;
            }
          }
          var mnuH = element.height();
          if (range != -1) {
            if (scrollTop + offset >= ranges[range].from) {
              if (scrollTop + offset + mnuH < ranges[range].to) {
                return ST_DOCKED;
              }
              else {
                return ST_HALT_ABOVE;
              }
            }
          }
          else {
            if (nextRange != -1) {
              return ST_HALT_BELOW;
            }
          }
        }
        var onScroll = (function() {
          var lock = false;
          var prevState = ST_HALT_BELOW;
          var state = prevState;
          return function() {
            if (lock) return;
            prevState = state;
            state = getState();
            if (ranges.length === 0) {
              ranges.push({
                from: 0,
                to: pageH
              });
            }
            var scrollTop = $scope.scrollable.scrollTop();
            var offset = $scope.scrollOffset;
            var mnuH = element.height();
            cssClasses.remove("menu_docked");
            cssClasses.remove("menu_halt_above");
            cssClasses.remove("menu_halt_below");
            switch (state) {
              case ST_DOCKED:
                offsetY = offset;
                cssClasses.add("menu_docked");
                break;
              case ST_HALT_ABOVE:
                cssClasses.add("menu_halt_above");
                if (prevState == ST_HALT_BELOW) {
                  cssClasses.add("menu_fade_out");
                  $interval(function() {
                    offsetY = ranges[range].to - mnuH - scrollTop;
                    cssClasses.remove("menu_fade_out");
                    cssClasses.add("menu_fade_in");
                    $interval(function() {
                        cssClasses.remove("menu_fade_in");
                    }, 500, 1);
                    lock = false;
                  }, 300, 1);
                  lock = true;
                }
                else {
                  offsetY = ranges[range].to - mnuH - scrollTop;
                }
                break;
              case ST_HALT_BELOW:
                offsetY = ranges[nextRange].from - scrollTop;
                cssClasses.add("menu_halt_below");
                if (prevState == ST_HALT_ABOVE) {
                  cssClasses.add("menu_fade_in");
                  $interval(function() {
                    cssClasses.remove("menu_fade_in");
                  }, 500, 1);
                }
                break;
            }
          };
        })();
        this.addRange = function(range) {
          ranges.push(range);
        };
        $scope.$watch(onScroll);
        $scope.menuOffset = function() {
          var x = 0;
          if ($scope.scrollable && !$scope.scrollableIsWindow) {
            x = $scope.scrollable.scrollTop() - $scope.scrollable.offset().top;
          }
          return offsetY - x;
        };
        $scope.menuState = function() {
          return cssClasses.get();
        };
        $scope.menuItemState = function(sectionId) {
          var x = 0;
          if ($scope.scrollable && !$scope.scrollableIsWindow) {
            x = $scope.scrollable.scrollTop() - $scope.scrollable.offset().top;
          }
          var section = angular.element("#" + sectionId);
          if (section.length === 0) {
            $log.error("No element with id '" + sectionId + "'");
            return "item_inactive";
          }
          var sectionTop = section.offset().top + x;
          var sectionBtm = sectionTop + section.height();
          var scrollTop = $scope.scrollable.scrollTop() + $scope.scrollOffset;
          return (scrollTop >= sectionTop && scrollTop < sectionBtm) ? "item_active" : "item_inactive";
        };
        $scope.submenuState = function(sectionId) {
          var x = 0;
          if ($scope.scrollable && !$scope.scrollableIsWindow) {
            x = $scope.scrollable.scrollTop() - $scope.scrollable.offset().top;
          }
          var section = angular.element("#" + sectionId);
          if (section.length === 0) {
            $log.error("No element with id '" + sectionId + "'");
            return "item_inactive";
          }
          var sectionTop = section.offset().top + x;
          var sectionBtm = sectionTop + section.height();
          var scrollTop = $scope.scrollable.scrollTop() + $scope.scrollOffset;
          return (scrollTop >= sectionTop && scrollTop < sectionBtm) ? "submenu_active" : "submenu_inactive";
        };
      }],
      replace: true,
      transclude: true,
      template: "<div ng-transclude='ng-transclude'></div>",
      link: link
    };
  }])
  .directive("jlMenuRange", ["$log", function($log) {
    function link(scope, element, attrs, jlMenuCtrl) {
      // Note that this assumes that the DOM doesn't change, that
      // everything remains in the same place after page load.
      var fromY = 0, fromOffset = 0;
      var toY = 0, toOffset = 0;
      if (attrs.jlFromElement) {
        var e = angular.element(attrs.jlFromElement);
        if (e.length > 0) {
          if (attrs.jlFromAttr == "top") {
            fromY = e.offset().top;
          }
          else if (attrs.jlFromAttr == "bottom") {
            fromY = e.offset().top + e.height();
          }
          else { // Default to 'top'
            fromY = e.offset().top;
          }
        }
        else {
          $log.error("Could not find element '" + attrs.jlFromElement + "'");
        }
      }
      else {
        $log.warn("jlMenuRange directive expects jlFromElement attribute");
      }
      if (attrs.jlFromOffset) {
        fromOffset = parseInt(attrs.jlFromOffset);
      }
      else {
        fromOffset = 0;
      }
      if (attrs.jlToElement) {
        var e_ = angular.element(attrs.jlToElement);
        if (e_.length > 0) {
          if (attrs.jlToAttr == "top") {
            toY = e_.offset().top;
          }
          else if (attrs.jlToAttr == "bottom") {
            toY = e_.offset().top + e_.height();
          }
          else { // Default to 'top'
            toY = e_.offset().top;
          }
        }
        else {
          $log.error("Could not find element '" + attrs.jlFromElement + "'");
        }
      }
      else {
        $log.warn("jlMenuRange directive expects jlToElement attribute");
        toY = angular.element("body").height();
      }
      if (attrs.jlToOffset) {
        toOffset = parseInt(attrs.jlToOffset);
      }
      else {
        toOffset = 0;
      }
      jlMenuCtrl.addRange({
        from: fromY + fromOffset,
        to: toY + toOffset
      });
    }
    return {
      restrict: "E",
      require: "^jlMenu",
      replace: true,
      template: "<span></span>",
      link: link
    };
  }])
  .directive("jlScrollTo", ["$log", function($log) {
    function link(scope, element, attrs) {
      if (!attrs.jlScrollTo) {
        $log.error("jlScrollTo directive expects destination element id argument, e.g. 'sectionB'");
        return;
      }
      var scrollable = angular.element("html, body");
      if (attrs.jlScrollableParent && attrs.jlScrollableParent != "window") {
        scrollable = angular.element(attrs.jlScrollableParent);
        if (scrollable.length === 0) {
          $log.warn("Could not find element '" + attrs.jlScrollableParent + "'");
          scrollable = angular.element("html, body");
        }
      }
      var elem = angular.element(element);
      var destElem = angular.element("#" + attrs.jlScrollTo);
      if (destElem.length === 0) {
        $log.warn("Element with id '" + attrs.jlScrollTo + "' not found");
        return;
      }
      var destY = destElem.offset().top;
      elem.bind("click", function() {
        var h = Math.abs(destY - scrollable.scrollTop());
        var pps = 1600; // pixels per second
        var t = h / pps;
        scrollable.animate({
          scrollTop: destY
        }, t * 1000);
        return false;
      });
    }
    return {
      restrict: "A",
      link: link
    };
  }]);

我的解决方案如下:

"use strict";
describe("Menu", function() {
  beforeEach(module("Menu"));
  describe("jlScrollTo directive", function() {
    var scope, $compile;
    beforeEach(inject(function($rootScope, _$compile_) {
      scope = $rootScope.$new();
      $compile = _$compile_;
    }));
    it("should scroll to destination upon click", function() {
      var html = "";
      html += "<div id='wrap'>";
      html += "  <div id='scrollWin' style='position: absolute; top: 0; height: 100px; overflow-y: scroll'>";
      html += "    <div id='section1' style='height: 300px'>";
      html += "      <a id='buttonA' href='' jl-scrollable-parent='#scrollWin' jl-scroll-to='#section2'>Click me</a>";
      html += "      <a id='buttonB' href='' jl-scrollable-parent='#scrollWin' jl-scroll-to='#section5'>Click me</a>";
      html += "    </div>";
      html += "    <div id='section2' style='height: 300px'></div>";
      html += "    <div id='section3' style='height: 300px'></div>";
      html += "    <div id='section4' style='height: 300px'></div>";
      html += "    <div id='section5' style='height: 300px'></div>";
      html += "  </div>";
      html += "</div>";
      var element = angular.element(html);
      element.appendTo(document.body)
      var doc = $compile(element)(scope);
      var eScrollWin = doc.find("#scrollWin");
      var eButtonA = doc.find("#buttonA");
      var eButtonB = doc.find("#buttonB");
      $.fx.off = true;
      expect(eScrollWin.scrollTop()).toEqual(0);
      eButtonA.triggerHandler("click");
      expect(eScrollWin.scrollTop()).toEqual(300);
      eButtonB.triggerHandler("click");
      expect(eScrollWin.scrollTop()).toEqual(1200);
      eButtonA.triggerHandler("click");
      expect(eScrollWin.scrollTop()).toEqual(300);
      $.fx.off = false;
      document.body.removeChild(element.get(0));
    });
  });
  describe("jlMenuRange directive", function() {
    var scope, $compile;
    beforeEach(inject(function($rootScope, _$compile_) {
      scope = $rootScope.$new();
      $compile = _$compile_;
    }));
    it("should set ranges in jlMenu's controller", function() {
      var html = "";
      html += "<div id='wrap' style='position: absolute; padding: 0; top: 0'>";
      html += "<jl-menu id='menu' jl-scrollable-parent='#wrap' jl-fixed-offset-element='window' jl-fixed-offset-y='{{0}}' jl-scroll-offset='0' style='position: absolute; top: 0; height: 0'>";
      html += "</jl-menu>";
      html += "<div id='section1' style='height: 300px'></div>";
      html += "<div id='section2' style='height: 300px'></div>";
      html += "<div id='section3' style='height: 300px'></div>";
      html += "<div id='section4' style='height: 300px'></div>";
      html += "<div id='section5' style='height: 300px'></div>";
      html += "</div>";
      var eWrap = angular.element(html);
      eWrap.appendTo(document.body)
      var eJlMenu = eWrap.find("#menu");
      var cmpJlMenu = $compile(eJlMenu)(scope);
      var jlMenuCtrl = cmpJlMenu.controller("jlMenu");
      spyOn(jlMenuCtrl, "addRange");
      var jlMenuRange = "";
      jlMenuRange += "<jl-menu-range jl-from-element='#section1' jl-to-element='#section2'></jl-menu-range>";
      jlMenuRange += "<jl-menu-range jl-from-element='#section3' jl-to-element='#section4' jl-to-attr='bottom'></jl-menu-range>";
      var eJlMenuRange = angular.element(jlMenuRange);
      cmpJlMenu.append(eJlMenuRange);
      var cmpJlMenuRange = $compile(eJlMenuRange)(scope);
      expect(jlMenuCtrl.addRange).toHaveBeenCalledWith({ from: 0, to: 300 });
      expect(jlMenuCtrl.addRange).toHaveBeenCalledWith({ from: 600, to: 1200 });
      document.body.removeChild(eWrap.get(0));
    });
  });
  describe("jlMenu directive", function() {
    var scope, element, doc, common;
    var offset = 60;
    beforeEach(inject(function($rootScope, $compile, _common_) {
      scope = $rootScope.$new();
      common = _common_;
      var html = "";
      html += "<div id='outer-wrap' style='position: absolute; top: 100px; left: 0; width: 100%; height: 100%; overflow: hidden;'>";
      html += "  <div id='wrap' style='top: 0; left: 0; width: 100%; height: 100%; overflow-y: scroll;'>";
      html += "    <div id='sidebar' style='position: absolute; width: 200px; left: 0; top: 0; height: 100%; padding: 0 0 0 10px; z-index: 1;'>";
      html += "      <jl-menu id='menu' jl-scrollable-parent='#wrap' jl-scroll-offset='" + offset + "' style='position: absolute; top: 0; margin: 0; padding: 0;'>";
      html += "        <jl-menu-range jl-container-element='#wrap' jl-from-element='#section1' jl-from-offset='0' jl-from-attr='top' jl-to-element='#section3' jl-to-offset='0' jl-to-attr='top'></jl-menu-range>";
      html += "        <jl-menu-range jl-container-element='#wrap' jl-from-element='#section4' jl-from-offset='0' jl-from-attr='top' jl-to-element='#section5' jl-to-offset='0' jl-to-attr='top'></jl-menu-range>";
      html += "        <jl-menu-range jl-container-element='#wrap' jl-from-element='#section6' jl-from-offset='0' jl-from-attr='top' jl-to-element='#section6' jl-to-offset='0' jl-to-attr='bottom'></jl-menu-range>";
      html += "        <ul class='menu'>";
      html += "          <li>";
      html += "            <a href='#section1'>Section 1</a>";
      html += "            <ul class='submenu'>";
      html += "              <li>";
      html += "                <a href='#section1_1'>Section1, 1</a>";
      html += "              </li>";
      html += "              <li>";
      html += "                <a href='#section1_2'>Section1, 2</a>";
      html += "              </li>";
      html += "              <li>";
      html += "                <a href='#section1_3'>Section1, 3</a>";
      html += "              </li>";
      html += "            </ul>";
      html += "          </li>";
      html += "          <li>";
      html += "            <a href='#section2'>Section 2</a>";
      html += "            <ul class='submenu'>";
      html += "              <li>";
      html += "                <a href='#section2_1'>Section2, 1</a>";
      html += "              </li>";
      html += "              <li>";
      html += "                <a href='#section2_2'>Section2, 2</a>";
      html += "              </li>";
      html += "              <li>";
      html += "                <a href='#section2_3'>Section2, 3</a>";
      html += "              </li>";
      html += "            </ul>";
      html += "          </li>";
      html += "          <li>";
      html += "            <a href='#section3'>Section 3</a>";
      html += "          <li>";
      html += "            <a href='#section4'>Section 4</a>";
      html += "            <ul class='submenu'>";
      html += "              <li>";
      html += "                <a href='#section4_1'>Section4, 1</a>";
      html += "              </li>";
      html += "              <li>";
      html += "                <a href='#section4_2'>Section4, 2</a>";
      html += "              </li>";
      html += "              <li>";
      html += "                <a href='#section4_3'>Section4, 3</a>";
      html += "              </li>";
      html += "            </ul>";
      html += "          </li>";
      html += "          <li>";
      html += "            <a href='#section5'>Section 5</a>";
      html += "          <li>";
      html += "            <a href='#section6'>Section 6</a>";
      html += "            <ul class='submenu'>";
      html += "              <li>";
      html += "                <a href='#section6_1'>Section6, 1</a>";
      html += "              </li>";
      html += "              <li>";
      html += "                <a href='#section6_2'>Section6, 2</a>";
      html += "              </li>";
      html += "              <li>";
      html += "                <a href='#section6_3'>Section6, 3</a>";
      html += "              </li>";
      html += "            </ul>";
      html += "          </li>";
      html += "        </ul>";
      html += "      </jl-menu>";
      html += "    </div>";
      html += "    <div id='section1'>";
      html += "      <div class='section' id='section1_1' style='height: 300px;'></div>";
      html += "      <div class='section' id='section1_2' style='height: 300px;'></div>";
      html += "      <div class='section' id='section1_3' style='height: 300px;'></div>";
      html += "    </div>";
      html += "    <div id='section2'>";
      html += "      <div class='section' id='section2_1' style='height: 300px;'></div>";
      html += "      <div class='section' id='section2_2' style='height: 300px;'></div>";
      html += "      <div class='section' id='section2_3' style='height: 300px;'></div>";
      html += "    </div>";
      html += "    <div id='section3' style='position: relative; height: 500px;'></div>";
      html += "    <div id='section4'>";
      html += "      <div class='section' id='section4_1' style='height: 300px;'></div>";
      html += "      <div class='section' id='section4_2' style='height: 300px;'></div>";
      html += "      <div class='section' id='section4_3' style='height: 300px;'></div>";
      html += "    </div>";
      html += "    <div id='section5' style='position: relative; height: 500px;'></div>";
      html += "    <div id='section6'>";
      html += "      <div class='section' id='section6_1' style='height: 300px;'></div>";
      html += "      <div class='section' id='section6_2' style='height: 300px;'></div>";
      html += "      <div class='section' id='section6_3' style='height: 300px;'></div>";
      html += "    </div>";
      html += "  </div>";
      html += "</div>";
      element = angular.element(html);
      element.appendTo(document.body)
      doc = $compile(element)(scope);
    }));
    it("should remain docked when within range", function() {
      var eWrap = doc.find("#wrap");
      var eMenu = doc.find("#menu");
      $.fx.off = true;
      eWrap.scrollTop(310);
      eWrap.triggerHandler("scroll");
      scope.$digest();
      expect(scope.menuState()).toEqual("menu_docked");
      expect(scope.menuOffset()).toEqual(offset);
      expect(scope.submenuState("#section1")).toEqual("submenu_active");
      expect(scope.submenuState("#section2")).toEqual("submenu_inactive");
      expect(scope.submenuState("#section4")).toEqual("submenu_inactive");
      expect(scope.submenuState("#section6")).toEqual("submenu_inactive");
      expect(scope.menuItemState("#section1")).toEqual("item_active");
      expect(scope.menuItemState("#section1_1")).toEqual("item_inactive");
      expect(scope.menuItemState("#section1_2")).toEqual("item_active");
      expect(scope.menuItemState("#section1_3")).toEqual("item_inactive");
      expect(scope.menuItemState("#section2")).toEqual("item_inactive");
      expect(scope.menuItemState("#section2_1")).toEqual("item_inactive");
      expect(scope.menuItemState("#section2_2")).toEqual("item_inactive");
      expect(scope.menuItemState("#section2_3")).toEqual("item_inactive");
      expect(scope.menuItemState("#section3")).toEqual("item_inactive");
      expect(scope.menuItemState("#section4")).toEqual("item_inactive");
      expect(scope.menuItemState("#section4_1")).toEqual("item_inactive");
      expect(scope.menuItemState("#section4_2")).toEqual("item_inactive");
      expect(scope.menuItemState("#section4_3")).toEqual("item_inactive");
      expect(scope.menuItemState("#section5")).toEqual("item_inactive");
      expect(scope.menuItemState("#section6")).toEqual("item_inactive");
      expect(scope.menuItemState("#section6_1")).toEqual("item_inactive");
      expect(scope.menuItemState("#section6_2")).toEqual("item_inactive");
      expect(scope.menuItemState("#section6_3")).toEqual("item_inactive");
      $.fx.off = false;
      document.body.removeChild(element.get(0));
    });
    it("should stick to bottom of range when approaching end of range", function() {
      var eWrap = doc.find("#wrap");
      var eMenu = doc.find("#menu");
      var eSection3 = doc.find("#section3");
      var rangeEnd = common.yPosWithinScrollable(eSection3, eWrap);
      $.fx.off = true;
      eWrap.scrollTop(rangeEnd - offset - 50);
      eWrap.triggerHandler("scroll");
      scope.$digest();
      expect(scope.menuState()).toEqual("menu_halt_above");
      expect(scope.menuOffset()).toEqual(common.fixedOffsetY(eSection3, eWrap) - eMenu.height());
      expect(scope.submenuState("#section1")).toEqual("submenu_inactive");
      expect(scope.submenuState("#section2")).toEqual("submenu_active");
      expect(scope.submenuState("#section4")).toEqual("submenu_inactive");
      expect(scope.submenuState("#section6")).toEqual("submenu_inactive");
      expect(scope.menuItemState("#section1")).toEqual("item_inactive");
      expect(scope.menuItemState("#section1_1")).toEqual("item_inactive");
      expect(scope.menuItemState("#section1_2")).toEqual("item_inactive");
      expect(scope.menuItemState("#section1_3")).toEqual("item_inactive");
      expect(scope.menuItemState("#section2")).toEqual("item_active");
      expect(scope.menuItemState("#section2_1")).toEqual("item_inactive");
      expect(scope.menuItemState("#section2_2")).toEqual("item_inactive");
      expect(scope.menuItemState("#section2_3")).toEqual("item_active");
      expect(scope.menuItemState("#section3")).toEqual("item_inactive");
      expect(scope.menuItemState("#section4")).toEqual("item_inactive");
      expect(scope.menuItemState("#section4_1")).toEqual("item_inactive");
      expect(scope.menuItemState("#section4_2")).toEqual("item_inactive");
      expect(scope.menuItemState("#section4_3")).toEqual("item_inactive");
      expect(scope.menuItemState("#section5")).toEqual("item_inactive");
      expect(scope.menuItemState("#section6")).toEqual("item_inactive");
      expect(scope.menuItemState("#section6_1")).toEqual("item_inactive");
      expect(scope.menuItemState("#section6_2")).toEqual("item_inactive");
      expect(scope.menuItemState("#section6_3")).toEqual("item_inactive");
      $.fx.off = false;
      document.body.removeChild(element.get(0));
    });
    it("should fade in at top of next range when exiting range", function() {
      var eWrap = doc.find("#wrap");
      var eMenu = doc.find("#menu");
      var eSection3 = doc.find("#section3");
      var eSection4 = doc.find("#section4");
      var rangeEnd = common.yPosWithinScrollable(eSection3, eWrap);
      $.fx.off = true;
      eWrap.scrollTop(rangeEnd - offset - 1);
      eWrap.triggerHandler("scroll");
      scope.$digest();
      eWrap.scrollTop(rangeEnd - offset);
      eWrap.triggerHandler("scroll");
      scope.$digest();
      var nextRangeStart = common.fixedOffsetY(eSection4, eWrap);
      expect(scope.menuState()).toEqual("menu_fade_in menu_halt_below");
      expect(scope.menuOffset()).toEqual(nextRangeStart);
      expect(scope.submenuState("#section1")).toEqual("submenu_inactive");
      expect(scope.submenuState("#section2")).toEqual("submenu_inactive");
      expect(scope.submenuState("#section4")).toEqual("submenu_inactive");
      expect(scope.submenuState("#section6")).toEqual("submenu_inactive");
      expect(scope.menuItemState("#section1")).toEqual("item_inactive");
      expect(scope.menuItemState("#section1_1")).toEqual("item_inactive");
      expect(scope.menuItemState("#section1_2")).toEqual("item_inactive");
      expect(scope.menuItemState("#section1_3")).toEqual("item_inactive");
      expect(scope.menuItemState("#section2")).toEqual("item_inactive");
      expect(scope.menuItemState("#section2_1")).toEqual("item_inactive");
      expect(scope.menuItemState("#section2_2")).toEqual("item_inactive");
      expect(scope.menuItemState("#section2_3")).toEqual("item_inactive");
      expect(scope.menuItemState("#section3")).toEqual("item_active");
      expect(scope.menuItemState("#section4")).toEqual("item_inactive");
      expect(scope.menuItemState("#section4_1")).toEqual("item_inactive");
      expect(scope.menuItemState("#section4_2")).toEqual("item_inactive");
      expect(scope.menuItemState("#section4_3")).toEqual("item_inactive");
      expect(scope.menuItemState("#section5")).toEqual("item_inactive");
      expect(scope.menuItemState("#section6")).toEqual("item_inactive");
      expect(scope.menuItemState("#section6_1")).toEqual("item_inactive");
      expect(scope.menuItemState("#section6_2")).toEqual("item_inactive");
      expect(scope.menuItemState("#section6_3")).toEqual("item_inactive");
      $.fx.off = false;
      document.body.removeChild(element.get(0));
    });
    it("should fade out and reappear at bottom of above range when scrolling upward into range", function() {
      var eWrap = doc.find("#wrap");
      var eMenu = doc.find("#menu");
      var eSection3 = doc.find("#section3");
      var eSection4 = doc.find("#section4");
      var rangeAEnd = common.yPosWithinScrollable(eSection3, eWrap);
      $.fx.off = true;
      eWrap.scrollTop(rangeAEnd - offset + 100);
      eWrap.triggerHandler("scroll");
      scope.$digest();
      var rangeBStart_fixed = common.fixedOffsetY(eSection4, eWrap);
      expect(scope.menuState()).toEqual("menu_halt_below");
      expect(scope.menuOffset()).toEqual(rangeBStart_fixed);
      expect(scope.submenuState("#section1")).toEqual("submenu_inactive");
      expect(scope.submenuState("#section2")).toEqual("submenu_inactive");
      expect(scope.submenuState("#section4")).toEqual("submenu_inactive");
      expect(scope.submenuState("#section6")).toEqual("submenu_inactive");
      expect(scope.menuItemState("#section1")).toEqual("item_inactive");
      expect(scope.menuItemState("#section1_1")).toEqual("item_inactive");
      expect(scope.menuItemState("#section1_2")).toEqual("item_inactive");
      expect(scope.menuItemState("#section1_3")).toEqual("item_inactive");
      expect(scope.menuItemState("#section2")).toEqual("item_inactive");
      expect(scope.menuItemState("#section2_1")).toEqual("item_inactive");
      expect(scope.menuItemState("#section2_2")).toEqual("item_inactive");
      expect(scope.menuItemState("#section2_3")).toEqual("item_inactive");
      expect(scope.menuItemState("#section3")).toEqual("item_active");
      expect(scope.menuItemState("#section4")).toEqual("item_inactive");
      expect(scope.menuItemState("#section4_1")).toEqual("item_inactive");
      expect(scope.menuItemState("#section4_2")).toEqual("item_inactive");
      expect(scope.menuItemState("#section4_3")).toEqual("item_inactive");
      expect(scope.menuItemState("#section5")).toEqual("item_inactive");
      expect(scope.menuItemState("#section6")).toEqual("item_inactive");
      expect(scope.menuItemState("#section6_1")).toEqual("item_inactive");
      expect(scope.menuItemState("#section6_2")).toEqual("item_inactive");
      expect(scope.menuItemState("#section6_3")).toEqual("item_inactive");
      eWrap.scrollTop(rangeAEnd - offset - 100);
      eWrap.triggerHandler("scroll");
      scope.$digest();
      rangeBStart_fixed = common.fixedOffsetY(eSection4, eWrap);
      expect(scope.menuState()).toEqual("menu_fade_out menu_halt_below");
      expect(scope.menuOffset()).toEqual(rangeBStart_fixed);
      expect(scope.submenuState("#section1")).toEqual("submenu_inactive");
      expect(scope.submenuState("#section2")).toEqual("submenu_active");
      expect(scope.submenuState("#section4")).toEqual("submenu_inactive");
      expect(scope.submenuState("#section6")).toEqual("submenu_inactive");
      expect(scope.menuItemState("#section1")).toEqual("item_inactive");
      expect(scope.menuItemState("#section1_1")).toEqual("item_inactive");
      expect(scope.menuItemState("#section1_2")).toEqual("item_inactive");
      expect(scope.menuItemState("#section1_3")).toEqual("item_inactive");
      expect(scope.menuItemState("#section2")).toEqual("item_active");
      expect(scope.menuItemState("#section2_1")).toEqual("item_inactive");
      expect(scope.menuItemState("#section2_2")).toEqual("item_inactive");
      expect(scope.menuItemState("#section2_3")).toEqual("item_active");
      expect(scope.menuItemState("#section3")).toEqual("item_inactive");
      expect(scope.menuItemState("#section4")).toEqual("item_inactive");
      expect(scope.menuItemState("#section4_1")).toEqual("item_inactive");
      expect(scope.menuItemState("#section4_2")).toEqual("item_inactive");
      expect(scope.menuItemState("#section4_3")).toEqual("item_inactive");
      expect(scope.menuItemState("#section5")).toEqual("item_inactive");
      expect(scope.menuItemState("#section6")).toEqual("item_inactive");
      expect(scope.menuItemState("#section6_1")).toEqual("item_inactive");
      expect(scope.menuItemState("#section6_2")).toEqual("item_inactive");
      expect(scope.menuItemState("#section6_3")).toEqual("item_inactive");
      $.fx.off = false;
      document.body.removeChild(element.get(0));
    });
  });
});