Angularjs 单元测试使用外部模板解析自定义指令内的承诺

Angularjs unit test resolve promise inside custom directive with external template

我有一个使用外部模板并从服务传递数据的自定义指令。我决定确保在修改数据之前解决了承诺,这在实际代码中很好但破坏了我的单元测试,这很烦人。我尝试了多种变体,但现在卡住了。我正在使用 'ng-html2js' 预处理器。

这是单元测试

describe('ccAccordion', function () {
var elm, scope, deferred, promise, things;
beforeEach(module('ccAccordion'));
// load the templates
beforeEach(module('components/accordion/accordion.html'));

beforeEach(inject(function ($rootScope, $compile, $q) {
    elm = angular.element(
      '<cc-accordion items="genres"></cc-accordion>'
      );
    scope = $rootScope;
    things = [{
            title: 'Scifi',
            description: 'Scifi description'
        }, {
            title: 'Comedy',
            description: 'Comedy description'
        }];

    deferred = $q.defer();
    promise = deferred.promise;

    promise.then(function (things) {
        scope.items = things;   
    });

    // Simulate resolving of promise
    deferred.resolve(things);

    // Propagate promise resolution to 'then' functions using $apply().
    scope.$apply();

    // compile the template?
    $compile(elm)(scope);
    scope.$digest();
}));

it('should create clickable titles', function () {
    var titles = elm.find('.cc-accord h2');

    expect(titles.length).toBe(2);
    expect(titles.eq(0).text().trim()).toBe('Scifi');
    expect(titles.eq(1).text().trim()).toBe('Comedy');
});

我省略了自定义 addMatchers 和其余测试。我得到的错误是

TypeError: 'undefined' is not an object (evaluating 'scope.items.$promise')

这是指令

var ccAccordion = angular.module("ccAccordion", []);
ccAccordion.directive("ccAccordion", function () {
return {
    restrict: "AE",
    templateUrl: "components/accordion/accordion.html",
    scope: {
        items: "="
    },
    link: function (scope) {
        scope.items.$promise.then(function (items) {
            angular.forEach(scope.items, function (item) {
                item.selected = false;
            });
            items[0].selected = true;
        });

        scope.select = function (desiredItem) {
            (desiredItem.selected === true) ? desiredItem.selected = false : desiredItem.selected = true;
            angular.forEach(scope.items, function (item) {
                if (item !== desiredItem) {
                    item.selected = false;
                }
            });
        };

    }
};

});

这是main.html

中使用指令的地方
<cc-accordion items="genres"></cc-accordion>

在主控制器中传入流派服务ie

 angular.module('magicApp')
.controller('GenresCtrl', ['$scope', 'BREAKPOINTS', 'Genre', 
function ($scope, BREAKPOINTS, Genre) {
    $scope.bp = BREAKPOINTS;
    $scope.genres = Genre.query();
}]);

好的,我会将您在 link 中输入的代码移动到控制器中。数据处理可能应该发生在服务中。我知道有人告诉您大型控制器不好,但大型 linking 功能通常更差,永远不应该进行那种数据处理。

.controller('GenresCtrl', ['$scope', 'BREAKPOINTS', 'Genre', 
function ($scope, BREAKPOINTS, Genre) {
    $scope.bp = BREAKPOINTS;
    $scope.genres = Genre.query().then(function (items) {
        angular.forEach(scope.items, function (item) {
            item.selected = false;
        });
        items[0].selected = true;
    });

    scope.select = function (desiredItem) {
        (desiredItem.selected === true) ? desiredItem.selected = false : desiredItem.selected = true;
        angular.forEach(scope.items, function (item) {
            if (item !== desiredItem) {
                item.selected = false;
            }
        });
    };
});

您的 link 函数现在是空的。相反,在 rootScope 上定义项目,这可确保 isolateScope 和您的指令接口正常工作。

beforeEach(inject(function ($rootScope, $compile, $q) {
    elm = angular.element(
      '<cc-accordion items="genres"></cc-accordion>'
      );
    scope = $rootScope;
    things = [{
            title: 'Scifi',
            description: 'Scifi description'
        }, {
            title: 'Comedy',
            description: 'Comedy description'
        }];
    scope.items = things; // Tests your directive interface

    // compile the template?
    $compile(elm)(scope);
    scope.$digest();
}));

应在控制器测试中通过模拟服务的 return 值来测试承诺的行为。您的 $promise 测试问题已解决。

实际问题是您假设 $q.defer() 给了您与 angular $http 相同的承诺,但这是通过设计解决的。

正如彼得所说,从指令中删除承诺并将其添加到控制器

angular.module('magicApp')
.controller('MainCtrl', ['$scope', 'Genre',
  function ($scope, Genre) {
      $scope.genres = Genre.query();
      $scope.genres.$promise.then(function () {
          angular.forEach($scope.genres, function (genre) {
              genre.selected = false;
          });
          $scope.genres[0].selected = true;
      });
  }]);

这也将允许控制器指定选择哪个选项卡开始。

在指令中

var ccAccordion = angular.module("ccAccordion", []);
ccAccordion.directive("ccAccordion", function () {
return {
    restrict: "AE",
    templateUrl: "components/accordion/accordion.html",
    scope: {
        items: "="
    },
    link: function (scope) {
        scope.select = function (desiredItem) {
            (desiredItem.selected === true) ? desiredItem.selected = false : desiredItem.selected = true;
            angular.forEach(scope.items, function (item) {
                if (item !== desiredItem) {
                    item.selected = false;
                }
            });
        };

    }
};

}); 指令单元测试现在看起来像这样

describe('ccAccordion', function () {
var elm, scope, deferred, promise, things;
beforeEach(module('ccAccordion'));

beforeEach(function () {
    jasmine.addMatchers({
        toHaveClass: function () {
            return {
                compare: function (actual, expected) {
                    var classTest = actual.hasClass(expected);
                    classTest ? classTest = true : classTest = false;
                    return {
                        pass: classTest,
                        message: 'Expected ' + angular.mock.dump(actual) + ' to have class ' + expected
                    };
                }
            };
        }
    });
});

// load the templates
beforeEach(module('components/accordion/accordion.html'));

beforeEach(inject(function ($rootScope, $compile, $q) {
    elm = angular.element(
      '<cc-accordion items="genres"></cc-accordion>'
      );
    scope = $rootScope;
    scope.genres = [{
            title: 'Scifi',
            description: 'Scifi description'
        }, {
            title: 'Comedy',
            description: 'Comedy description'
        }];
    $compile(elm)(scope);
    scope.$digest();
}));

it('should create clickable titles', function () {
    var titles = elm.find('.cc-accord h2');

    expect(titles.length).toBe(2);
    expect(titles.eq(0).text().trim()).toBe('Scifi');
    expect(titles.eq(1).text().trim()).toBe('Comedy');
});

it('should bind the content', function () {
 var contents = elm.find('.cc-accord-content div:first-child');

 expect(contents.length).toBe(2);
 expect(contents.eq(0).text().trim()).toBe('Scifi description');
 expect(contents.eq(1).text().trim()).toBe('Comedy description');

 });

 it('should change active content when header clicked', function () {
 var titles = elm.find('.cc-accord h2'),
 divs = elm.find('.cc-accord');

 // click the second header
 titles.eq(1).find('a').click();

 // second div should be active
 expect(divs.eq(0)).not.toHaveClass('active');
 expect(divs.eq(1)).toHaveClass('active');
 });

}); 并且主控制器的单元测试现在添加了 属性 of selected

'use-strict';

describe('magicApp controllers', function () {
// using addMatcher because $resource is not $http and returns a promise
beforeEach(function () {
    jasmine.addMatchers({
        toEqualData: function () {
            return {
                compare: function (actual, expected) {
                    return {
                        pass: angular.equals(actual, expected)
                    };
                }
            };
        }
    });
});

beforeEach(module('magicApp'));
beforeEach(module('magicServices'));


describe('MainCtrl', function () {
    var scope, ctrl, $httpBackend;

    beforeEach(inject(function (_$httpBackend_, $rootScope, $controller) {
        $httpBackend = _$httpBackend_;
        $httpBackend.expectGET('/api/genres').
          respond([{title: 'Scifi', selected: true}, {title: 'Comedy', selected: false}]);

        scope = $rootScope.$new();
        ctrl = $controller('MainCtrl', {$scope: scope});
    }));


    it('should create "genres" model with 2 genres fetched from xhr', function () {
        expect(scope.genres).toEqualData([]);
        $httpBackend.flush();

        expect(scope.genres).toEqualData(
          [{title: 'Scifi', selected: true}, {title: 'Comedy', selected: false}]);
    });
});

});