1

我有一个使用外部模板并从服务传递数据的自定义指令。我决定在修改数据之前确保承诺得到解决,这在实际代码中很好,但破坏了我的单元测试,这很烦人。我尝试了许多变体,但现在卡住了。我正在使用“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>

在主控制器中,流派服务被传入,即

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

2 回答 2

0

Okay, I would move that code you put in link into the controller. The data processing should probably happen in a service. I know you've been told big controllers are bad, but big linking functions are generally worse, and should never do that kind of data processing.

.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;
            }
        });
    };
});

Your link function is now empty. Define items on the rootScope instead, this ensures that the isolateScope and your directive interface are working correctly.

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();
}));

The behavior of the promise should be tested in a controller test, by mocking the return value of the service. Your problem with the $promise test has been solved.

The actual issue was that you were assuming that $q.defer() gave you the same kind of promise as the angular $http, but that is solved by design instead.

于 2015-01-23T08:37:00.873 回答
0

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

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');
 });

}); 并且主控制器的单元测试现在添加了 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}]);
    });
});

});

于 2015-01-23T11:17:35.597 回答