title: Testing AngularJS directive that modifies other scope in Karma / Jasmine
body: <p>Reference: <a href="https://stackoverflow.com/q/19121863/863924">Unit Testing AngularJS Directives: scopes not updating?</a></p>
  
  <h1>Case</h1>
  
  <p>I have a directive called <em>editable</em> that take an <code>ng-model</code> and creates a toggleable/editable field. The directive works and the parent scope is updated correctly, there are no problems in the actual function of the directive. I cannot seem to write a test that supports this though. It took me a long time to get the directive working properly with all the caveats so I really want to get some tests in place to make sure it continues to work in all of the different cases.</p>
  
  <h1>The Directive (which works)</h1>
  
  <p>I can't be sure which pieces will be relevant so i included the whole thing.</p>
  
  <pre><code>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();
                          }
                      });
  
                  };
              }
          };
      }
  ]);
  </code></pre>
  
  <h1>Spec</h1>
  
  <p>Fails at the end</p>
  
  <pre><code>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('\
              &lt;div editable="" ng-model="testValue"&gt;&lt;/div&gt;\
          ');
  
          // 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); // &lt;-fails
      });
  });
  </code></pre>
  
  <h1>So...</h1>
  
  <p>Everything seems to be working until I actually expect the parent scope to have the changed value.</p>
  
  <p>I know there is a lot here, I appreciate any guidance you can provide.</p>
  
  <p><a href="http://plnkr.co/edit/AqDNpjEkI7UH5WmYMWpF?p=preview" rel="nofollow noreferrer">Plunk of Angular + Jasmine with directive (work in progress)</a></p>