Модульный тест Angularjs разрешить обещание внутри пользовательской директивы с внешним шаблоном

У меня есть пользовательская директива, которая использует внешний шаблон и передает данные из службы. Я решил убедиться, что обещание было выполнено, прежде чем изменять данные, что было хорошо в реальном коде, но нарушило мои модульные тесты, что раздражает. Я попробовал несколько вариантов, но сейчас застрял. Я использую препроцессор '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();
}]);

2 ответа

Решение

Хорошо, я бы переместил код, который вы вставили в ссылку, в контроллер. Обработка данных, вероятно, должна происходить в службе. Я знаю, что вам говорили, что большие контроллеры плохие, но большие функции связывания, как правило, хуже и никогда не должны выполнять такую ​​обработку данных.

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

Ваша функция ссылки теперь пуста. Вместо этого определите элементы в 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();
}));

Поведение обещания должно быть проверено в тесте контроллера путем насмешки возвращаемого значения сервиса. Ваша проблема с $ обещанием теста была решена.

Фактическая проблема заключалась в том, что вы предполагали, что $q.defer() дал вам такое же обещание, что и угловое $ 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');
 });

}); И модульный тест для основного контроллера теперь имеет добавленное свойство 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}]);
    });
});

});

Другие вопросы по тегам