测试AngularJS指令,修改Karma / Jasmine中的其他范围

时间:2014-02-03 20:00:52

标签: angularjs angularjs-directive jasmine angularjs-scope karma-runner

参考:Unit Testing AngularJS Directives: scopes not updating?

案例

我有一个名为 editable 的指令,它带有ng-model并创建一个可切换/可编辑的字段。该指令有效,父作用域正确更新,指令的实际功能没有问题。我似乎无法写一个支持这个的测试。我花了很长时间才能使指令正常运行所有注意事项,所以我真的希望得到一些测试,以确保它能够在所有不同的情况下继续工作。

指令(有效)

我不能确定哪些部分是相关的,所以我把所有部分都包括在内。

app.directive('editable',
    ['$templateCache', '$compile',
    function ($templateCache, $compile) {
        return {
            restrict: 'A',
            transclude: true,
            templateUrl: 'template/directives/editable.html',
            replace: true,
            require: 'ngModel',
            scope: true,
            compile: function(element, attrs, transcludeFn) {

                return function (scope, iElement, iAttrs, ctrl) {

                    var validityId = 'editable-' + scope.$id;

                    scope.lastSavedValue = iElement.find('input').val();

                    scope.storeValue = function() {
                        scope.lastSavedValue = iElement.find('input').val();
                    };

                    scope.edit = function() {
                        scope.storeValue();
                        scope.editing = true;
                        $('input', iElement).focus().select();
                        ctrl.$setValidity(validityId, true);
                    };

                    scope.ok = function() {
                        var inputCtrl = iElement.find('input').controller('ngModel');
                        if(inputCtrl.$valid === true) {
                            scope.editing = false;
                            scope.value = inputCtrl.$viewValue;
                            ctrl.$setValidity(validityId, false);
                            ctrl.$setViewValue(inputCtrl.$viewValue); // Not sure (why) this is needed
                        }
                    };

                    scope['delete'] = function() {
                        scope.deleted = true;
                        scope.editing = false;
                    };

                    scope.undo = function() {
                        var inputCtrl = iElement.find('input').controller('ngModel');
                        if(scope.lastSavedValue) {
                            inputCtrl.$setViewValue(scope.lastSavedValue);
                            scope.value = scope.lastSavedValue;
                            ctrl.$setViewValue(scope.lastSavedValue);
                        }
                        iElement.find('input').val(scope.value);
                        scope.editing = false;
                        scope.deleted = false;
                        ctrl.$setValidity(validityId, false);
                    };

                    transcludeFn(scope, function(clone) {
                        var $editingReplacement = $(clone).filter('[editing]');
                        var $viewingReplacement = $(clone).filter('[viewing]');
                        var $deletedReplacement = $(clone).filter('[deleted]');
                        var $viewingControls = $('.editable-view-container .controls', iElement);
                        var $editingControls = $('.editable-input-container .controls', iElement);
                        var $deletedControls = $('.editable-delete-container .controls', iElement);
                        if($editingReplacement.length) {
                            $('.editable-input-container', iElement).html($editingReplacement.html());
                            $('.editable-input-container', iElement).append($editingControls);
                            $compile($('.editable-input-container', iElement))(scope);
                        } else {
                            $('.editable-input-container', iElement).find('input').attr('ng-model', iAttrs['ngModel']);
                            $compile($('.editable-input-container', iElement))(scope);
                        }
                        if($viewingReplacement.length) {
                            $('.editable-view-container', iElement).html($viewingReplacement.html());
                            $('.editable-view-container', iElement).append($viewingControls);
                            $compile($('.editable-view-container', iElement))(scope);
                        }
                        if($deletedReplacement.length) {
                            $('.editable-delete-container', iElement).html($deletedReplacement.html());
                            $('.editable-delete-container', iElement).append($deletedControls);
                        }
                    });

                    /**
                     * Deleted (Isolated Scope)
                     *
                     * Tracks if the user has clicked the delete button
                     * 
                     * @type {Boolean}
                     */
                    scope.deleted = false;

                    /**
                     * Editing (Isolated Scope)
                     *
                     * Tracks the state of the view
                     * 
                     * @type {Boolean}
                     */
                    scope.editing = false;

                    /**
                     * Initial Loader
                     *
                     * Run once after ctrl is loaded
                     * 
                     * @return {[type]} [description]
                     */
                    var unbindWatcher = scope.$watch(function() { return ctrl.$modelValue; }, function(newVal, oldVal) {
                        if(typeof ctrl.$modelValue !== 'undefined') {
                            scope.value = ctrl.$modelValue;
                            scope.editing = ctrl.$modelValue ? false : true;
                            unbindWatcher();
                        }
                    });

                };
            }
        };
    }
]);

规格

最后失败

describe('Editable Directive', function() {

    // Keep references to element and scope so that they are available to all tests
    var element, scope, ctrl;

    beforeEach(module('ltAccountApp'));

    beforeEach(module('templates'));

    beforeEach(inject(function ($rootScope, $compile) {

        var linkFn, el;

        // scope = $rootScope;
        scope = $rootScope.$new();

        scope.testValue = 'xxx';

        el = angular.element('\
            <div editable="" ng-model="testValue"></div>\
        ');

        // The $compile method returns the directive's link function
        linkFn = $compile(el);

        // The link function returns the resulting DOM object
        element = linkFn(scope);

        element.scope().$apply();

        ctrl = element.controller('ngModel');

    }));

    it('should assign input value to scope value', function() {
        expect(element.find('input').val()).toEqual(scope.testValue);
    });

    it('should have access to parent scope variable passed into directive', function() {
        expect(ctrl.$viewValue).toEqual(scope.testValue);
        expect(ctrl.$modelValue).toEqual(scope.testValue);
    });

    it('should manage state editing correctly', function() {
        expect(element.scope().editing).toBe(false);
        element.scope().edit();
        expect(element.scope().editing).toBe(true);
    });

    it('should manage state deleted correctly', function() {
        expect(element.scope().deleted).toBe(false);
        element.scope()['delete']();
        expect(element.scope().deleted).toBe(true);
    });

    it('should be able to modify parent scope variable passed into directive', function() {
        // Not sure what this does, added from referenced SO question
        // spyOn(scope, '$apply').andCallThrough();

        var newValue = 'yyy';

        element.scope().edit();

        element.find('input').val(newValue);
        element.find('input').trigger('input');

        element.scope().ok();

        expect(ctrl.$viewValue).toEqual(newValue);
        expect(ctrl.$modelValue).toEqual(newValue);
        expect(element.scope().value).toEqual(newValue);

        expect(scope.$apply).toHaveBeenCalled();

        expect(scope.testValue).toEqual(newValue); // <-fails
    });
});

所以...

在我实际期望父范围具有更改的值之前,一切似乎都在起作用。

我知道这里有很多,我感谢您提供的任何指导。

Plunk of Angular + Jasmine with directive (work in progress)

1 个答案:

答案 0 :(得分:1)

让我们说你必须将输入字段的值更新为'kasrak'。试着这样做:

var elm = element.find("input");
elm.val('kasrak');
elm.trigger($sniffer.hasEvent('input') ? 'input' : 'change');
$scope.$digest()

Key是$ sniffer服务的第三行。我在angular-ui的bootstrap测试中找到了这个。

以下是测试规范中函数的链接:https://github.com/angular-ui/bootstrap/blob/master/src/typeahead/test/typeahead.spec.js#L31