diff --git a/test/helpers/testabilityPatch.js b/test/helpers/testabilityPatch.js index fd55c58c..b349f123 100644 --- a/test/helpers/testabilityPatch.js +++ b/test/helpers/testabilityPatch.js @@ -339,3 +339,67 @@ window.dump = function() { return angular.mock.dump(arg); })); }; + +function getInputCompileHelper(currentSpec) { + + var helper = {}; + + module(function($compileProvider) { + $compileProvider.directive('attrCapture', function() { + return function(scope, element, $attrs) { + helper.attrs = $attrs; + }; + }); + }); + + inject(function($compile, $rootScope, $sniffer) { + + helper.compileInput = function(inputHtml, mockValidity, scope) { + + scope = helper.scope = scope || $rootScope; + + // Create the input element and dealoc when done + helper.inputElm = jqLite(inputHtml); + + // Set up mock validation if necessary + if (isObject(mockValidity)) { + VALIDITY_STATE_PROPERTY = 'ngMockValidity'; + helper.inputElm.prop(VALIDITY_STATE_PROPERTY, mockValidity); + currentSpec.after(function() { + VALIDITY_STATE_PROPERTY = 'validity'; + }); + } + + // Create the form element and dealoc when done + helper.formElm = jqLite('
'); + helper.formElm.append(helper.inputElm); + + // Compile the lot and return the input element + $compile(helper.formElm)(scope); + + spyOn(scope.form, '$addControl').andCallThrough(); + spyOn(scope.form, '$$renameControl').andCallThrough(); + + scope.$digest(); + + return helper.inputElm; + }; + + helper.changeInputValueTo = function(value) { + helper.inputElm.val(value); + browserTrigger(helper.inputElm, $sniffer.hasEvent('input') ? 'input' : 'change'); + }; + + helper.changeGivenInputTo = function(inputElm, value) { + inputElm.val(value); + browserTrigger(inputElm, $sniffer.hasEvent('input') ? 'input' : 'change'); + }; + + helper.dealoc = function() { + dealoc(helper.inputElm); + dealoc(helper.formElm); + }; + }); + + return helper; +} diff --git a/test/ng/directive/inputSpec.js b/test/ng/directive/inputSpec.js index f0341722..6f50fc13 100644 --- a/test/ng/directive/inputSpec.js +++ b/test/ng/directive/inputSpec.js @@ -1,1513 +1,33 @@ 'use strict'; -describe('NgModelController', function() { - /* global NgModelController: false */ - var ctrl, scope, ngModelAccessor, element, parentFormCtrl; - - beforeEach(inject(function($rootScope, $controller) { - var attrs = {name: 'testAlias', ngModel: 'value'}; - - parentFormCtrl = { - $$setPending: jasmine.createSpy('$$setPending'), - $setValidity: jasmine.createSpy('$setValidity'), - $setDirty: jasmine.createSpy('$setDirty'), - $$clearControlValidity: noop - }; - - element = jqLite('
'); - element.data('$formController', parentFormCtrl); - - scope = $rootScope; - ngModelAccessor = jasmine.createSpy('ngModel accessor'); - ctrl = $controller(NgModelController, { - $scope: scope, - $element: element.find('input'), - $attrs: attrs - }); - })); - - - afterEach(function() { - dealoc(element); - }); - - - it('should init the properties', function() { - expect(ctrl.$untouched).toBe(true); - expect(ctrl.$touched).toBe(false); - expect(ctrl.$dirty).toBe(false); - expect(ctrl.$pristine).toBe(true); - expect(ctrl.$valid).toBe(true); - expect(ctrl.$invalid).toBe(false); - - expect(ctrl.$viewValue).toBeDefined(); - expect(ctrl.$modelValue).toBeDefined(); - - expect(ctrl.$formatters).toEqual([]); - expect(ctrl.$parsers).toEqual([]); - - expect(ctrl.$name).toBe('testAlias'); - }); - - - describe('setValidity', function() { - - function expectOneError() { - expect(ctrl.$error).toEqual({someError: true}); - expect(ctrl.$$success).toEqual({}); - expect(ctrl.$pending).toBeUndefined(); - } - - function expectOneSuccess() { - expect(ctrl.$error).toEqual({}); - expect(ctrl.$$success).toEqual({someError: true}); - expect(ctrl.$pending).toBeUndefined(); - } - - function expectOnePending() { - expect(ctrl.$error).toEqual({}); - expect(ctrl.$$success).toEqual({}); - expect(ctrl.$pending).toEqual({someError: true}); - } - - function expectCleared() { - expect(ctrl.$error).toEqual({}); - expect(ctrl.$$success).toEqual({}); - expect(ctrl.$pending).toBeUndefined(); - } - - it('should propagate validity to the parent form', function() { - expect(parentFormCtrl.$setValidity).not.toHaveBeenCalled(); - ctrl.$setValidity('ERROR', false); - expect(parentFormCtrl.$setValidity).toHaveBeenCalledOnceWith('ERROR', false, ctrl); - }); - - it('should transition from states correctly', function() { - expectCleared(); - - ctrl.$setValidity('someError', false); - expectOneError(); - - ctrl.$setValidity('someError', undefined); - expectOnePending(); - - ctrl.$setValidity('someError', true); - expectOneSuccess(); - - ctrl.$setValidity('someError', null); - expectCleared(); - }); - - it('should set valid/invalid with multiple errors', function() { - ctrl.$setValidity('first', false); - expect(ctrl.$valid).toBe(false); - expect(ctrl.$invalid).toBe(true); - - ctrl.$setValidity('second', false); - expect(ctrl.$valid).toBe(false); - expect(ctrl.$invalid).toBe(true); - - ctrl.$setValidity('third', undefined); - expect(ctrl.$valid).toBe(undefined); - expect(ctrl.$invalid).toBe(undefined); - - ctrl.$setValidity('third', null); - expect(ctrl.$valid).toBe(false); - expect(ctrl.$invalid).toBe(true); - - ctrl.$setValidity('second', true); - expect(ctrl.$valid).toBe(false); - expect(ctrl.$invalid).toBe(true); - - ctrl.$setValidity('first', true); - expect(ctrl.$valid).toBe(true); - expect(ctrl.$invalid).toBe(false); - }); - - }); - - describe('setPristine', function() { - - it('should set control to its pristine state', function() { - ctrl.$setViewValue('edit'); - expect(ctrl.$dirty).toBe(true); - expect(ctrl.$pristine).toBe(false); - - ctrl.$setPristine(); - expect(ctrl.$dirty).toBe(false); - expect(ctrl.$pristine).toBe(true); - }); - }); - - describe('setDirty', function() { - - it('should set control to its dirty state', function() { - expect(ctrl.$pristine).toBe(true); - expect(ctrl.$dirty).toBe(false); - - ctrl.$setDirty(); - expect(ctrl.$pristine).toBe(false); - expect(ctrl.$dirty).toBe(true); - }); - - it('should set parent form to its dirty state', function() { - ctrl.$setDirty(); - expect(parentFormCtrl.$setDirty).toHaveBeenCalled(); - }); - }); - - describe('setUntouched', function() { - - it('should set control to its untouched state', function() { - ctrl.$setTouched(); - - ctrl.$setUntouched(); - expect(ctrl.$touched).toBe(false); - expect(ctrl.$untouched).toBe(true); - }); - }); - - describe('setTouched', function() { - - it('should set control to its touched state', function() { - ctrl.$setUntouched(); - - ctrl.$setTouched(); - expect(ctrl.$touched).toBe(true); - expect(ctrl.$untouched).toBe(false); - }); - }); - - describe('view -> model', function() { - - it('should set the value to $viewValue', function() { - ctrl.$setViewValue('some-val'); - expect(ctrl.$viewValue).toBe('some-val'); - }); - - - it('should pipeline all registered parsers and set result to $modelValue', function() { - var log = []; - - ctrl.$parsers.push(function(value) { - log.push(value); - return value + '-a'; - }); - - ctrl.$parsers.push(function(value) { - log.push(value); - return value + '-b'; - }); - - ctrl.$setViewValue('init'); - expect(log).toEqual(['init', 'init-a']); - expect(ctrl.$modelValue).toBe('init-a-b'); - }); - - - it('should fire viewChangeListeners when the value changes in the view (even if invalid)', - function() { - var spy = jasmine.createSpy('viewChangeListener'); - ctrl.$viewChangeListeners.push(spy); - ctrl.$setViewValue('val'); - expect(spy).toHaveBeenCalledOnce(); - spy.reset(); - - // invalid - ctrl.$parsers.push(function() {return undefined;}); - ctrl.$setViewValue('val2'); - expect(spy).toHaveBeenCalledOnce(); - }); - - - it('should reset the model when the view is invalid', function() { - ctrl.$setViewValue('aaaa'); - expect(ctrl.$modelValue).toBe('aaaa'); - - // add a validator that will make any input invalid - ctrl.$parsers.push(function() {return undefined;}); - expect(ctrl.$modelValue).toBe('aaaa'); - ctrl.$setViewValue('bbbb'); - expect(ctrl.$modelValue).toBeUndefined(); - }); - - it('should not reset the model when the view is invalid due to an external validator', function() { - ctrl.$setViewValue('aaaa'); - expect(ctrl.$modelValue).toBe('aaaa'); - - ctrl.$setValidity('someExternalError', false); - ctrl.$setViewValue('bbbb'); - expect(ctrl.$modelValue).toBe('bbbb'); - }); - - it('should not reset the view when the view is invalid', function() { - // this test fails when the view changes the model and - // then the model listener in ngModel picks up the change and - // tries to update the view again. - - // add a validator that will make any input invalid - ctrl.$parsers.push(function() {return undefined;}); - spyOn(ctrl, '$render'); - - // first digest - ctrl.$setViewValue('bbbb'); - expect(ctrl.$modelValue).toBeUndefined(); - expect(ctrl.$viewValue).toBe('bbbb'); - expect(ctrl.$render).not.toHaveBeenCalled(); - expect(scope.value).toBeUndefined(); - - // further digests - scope.$apply('value = "aaa"'); - expect(ctrl.$viewValue).toBe('aaa'); - ctrl.$render.reset(); - - ctrl.$setViewValue('cccc'); - expect(ctrl.$modelValue).toBeUndefined(); - expect(ctrl.$viewValue).toBe('cccc'); - expect(ctrl.$render).not.toHaveBeenCalled(); - expect(scope.value).toBeUndefined(); - }); - - it('should call parentForm.$setDirty only when pristine', function() { - ctrl.$setViewValue(''); - expect(ctrl.$pristine).toBe(false); - expect(ctrl.$dirty).toBe(true); - expect(parentFormCtrl.$setDirty).toHaveBeenCalledOnce(); - - parentFormCtrl.$setDirty.reset(); - ctrl.$setViewValue(''); - expect(ctrl.$pristine).toBe(false); - expect(ctrl.$dirty).toBe(true); - expect(parentFormCtrl.$setDirty).not.toHaveBeenCalled(); - }); - - it('should remove all other errors when any parser returns undefined', function() { - var a, b, val = function(val, x) { - return x ? val : x; - }; - - ctrl.$parsers.push(function(v) { return val(v, a); }); - ctrl.$parsers.push(function(v) { return val(v, b); }); - - ctrl.$validators.high = function(value) { - return !isDefined(value) || value > 5; - }; - - ctrl.$validators.even = function(value) { - return !isDefined(value) || value % 2 === 0; - }; - - a = b = true; - - ctrl.$setViewValue('3'); - expect(ctrl.$error).toEqual({ high: true, even: true }); - - ctrl.$setViewValue('10'); - expect(ctrl.$error).toEqual({}); - - a = undefined; - - ctrl.$setViewValue('12'); - expect(ctrl.$error).toEqual({ parse: true }); - - a = true; - b = undefined; - - ctrl.$setViewValue('14'); - expect(ctrl.$error).toEqual({ parse: true }); - - a = undefined; - b = undefined; - - ctrl.$setViewValue('16'); - expect(ctrl.$error).toEqual({ parse: true }); - - a = b = false; //not undefined - - ctrl.$setViewValue('2'); - expect(ctrl.$error).toEqual({ high: true }); - }); - - it('should not remove external validators when a parser failed', function() { - ctrl.$parsers.push(function(v) { return undefined; }); - ctrl.$setValidity('externalError', false); - ctrl.$setViewValue('someValue'); - expect(ctrl.$error).toEqual({ externalError: true, parse: true }); - }); - - it('should remove all non-parse-related CSS classes from the form when a parser fails', - inject(function($compile, $rootScope) { - - var element = $compile('
' + - '' + - '
')($rootScope); - var inputElm = element.find('input'); - var ctrl = $rootScope.myForm.myControl; - - var parserIsFailing = false; - ctrl.$parsers.push(function(value) { - return parserIsFailing ? undefined : value; - }); - - ctrl.$validators.alwaysFail = function() { - return false; - }; - - ctrl.$setViewValue('123'); - scope.$digest(); - - expect(element).toHaveClass('ng-valid-parse'); - expect(element).not.toHaveClass('ng-invalid-parse'); - expect(element).toHaveClass('ng-invalid-always-fail'); - - parserIsFailing = true; - ctrl.$setViewValue('12345'); - scope.$digest(); - - expect(element).not.toHaveClass('ng-valid-parse'); - expect(element).toHaveClass('ng-invalid-parse'); - expect(element).not.toHaveClass('ng-invalid-always-fail'); - - dealoc(element); - })); - - it('should set the ng-invalid-parse and ng-valid-parse CSS class when parsers fail and pass', function() { - var pass = true; - ctrl.$parsers.push(function(v) { - return pass ? v : undefined; - }); - - var input = element.find('input'); - - ctrl.$setViewValue('1'); - expect(input).toHaveClass('ng-valid-parse'); - expect(input).not.toHaveClass('ng-invalid-parse'); - - pass = undefined; - - ctrl.$setViewValue('2'); - expect(input).not.toHaveClass('ng-valid-parse'); - expect(input).toHaveClass('ng-invalid-parse'); - }); - - it('should update the model after all async validators resolve', inject(function($q) { - var defer; - ctrl.$asyncValidators.promiseValidator = function(value) { - defer = $q.defer(); - return defer.promise; - }; - - // set view value on first digest - ctrl.$setViewValue('b'); - - expect(ctrl.$modelValue).toBeUndefined(); - expect(scope.value).toBeUndefined(); - - defer.resolve(); - scope.$digest(); - - expect(ctrl.$modelValue).toBe('b'); - expect(scope.value).toBe('b'); - - // set view value on further digests - ctrl.$setViewValue('c'); - - expect(ctrl.$modelValue).toBe('b'); - expect(scope.value).toBe('b'); - - defer.resolve(); - scope.$digest(); - - expect(ctrl.$modelValue).toBe('c'); - expect(scope.value).toBe('c'); - - })); - - }); - - - describe('model -> view', function() { - - it('should set the value to $modelValue', function() { - scope.$apply('value = 10'); - expect(ctrl.$modelValue).toBe(10); - }); - - - it('should pipeline all registered formatters in reversed order and set result to $viewValue', - function() { - var log = []; - - ctrl.$formatters.unshift(function(value) { - log.push(value); - return value + 2; - }); - - ctrl.$formatters.unshift(function(value) { - log.push(value); - return value + ''; - }); - - scope.$apply('value = 3'); - expect(log).toEqual([3, 5]); - expect(ctrl.$viewValue).toBe('5'); - }); - - - it('should $render only if value changed', function() { - spyOn(ctrl, '$render'); - - scope.$apply('value = 3'); - expect(ctrl.$render).toHaveBeenCalledOnce(); - ctrl.$render.reset(); - - ctrl.$formatters.push(function() {return 3;}); - scope.$apply('value = 5'); - expect(ctrl.$render).not.toHaveBeenCalled(); - }); - - - it('should clear the view even if invalid', function() { - spyOn(ctrl, '$render'); - - ctrl.$formatters.push(function() {return undefined;}); - scope.$apply('value = 5'); - expect(ctrl.$render).toHaveBeenCalledOnce(); - }); - - it('should render immediately even if there are async validators', inject(function($q) { - spyOn(ctrl, '$render'); - ctrl.$asyncValidators.someValidator = function() { - return $q.defer().promise; - }; - - scope.$apply('value = 5'); - expect(ctrl.$viewValue).toBe(5); - expect(ctrl.$render).toHaveBeenCalledOnce(); - })); - - it('should not rerender nor validate in case view value is not changed', function() { - ctrl.$formatters.push(function(value) { - return 'nochange'; - }); - - spyOn(ctrl, '$render'); - ctrl.$validators.spyValidator = jasmine.createSpy('spyValidator'); - scope.$apply('value = "first"'); - scope.$apply('value = "second"'); - expect(ctrl.$validators.spyValidator).toHaveBeenCalledOnce(); - expect(ctrl.$render).toHaveBeenCalledOnce(); - }); - - }); - - describe('validation', function() { - - describe('$validate', function() { - it('should perform validations when $validate() is called', function() { - scope.$apply('value = ""'); - - var validatorResult = false; - ctrl.$validators.someValidator = function(value) { - return validatorResult; - }; - - ctrl.$validate(); - - expect(ctrl.$valid).toBe(false); - - validatorResult = true; - ctrl.$validate(); - - expect(ctrl.$valid).toBe(true); - }); - - it('should pass the last parsed modelValue to the validators', function() { - ctrl.$parsers.push(function(modelValue) { - return modelValue + 'def'; - }); - - ctrl.$setViewValue('abc'); - - ctrl.$validators.test = function(modelValue, viewValue) { - return true; - }; - - spyOn(ctrl.$validators, 'test'); - - ctrl.$validate(); - - expect(ctrl.$validators.test).toHaveBeenCalledWith('abcdef', 'abc'); - }); - - it('should set the model to undefined when it becomes invalid', function() { - var valid = true; - ctrl.$validators.test = function(modelValue, viewValue) { - return valid; - }; - - scope.$apply('value = "abc"'); - expect(scope.value).toBe('abc'); - - valid = false; - ctrl.$validate(); - - expect(scope.value).toBeUndefined(); - }); - - it('should update the model when it becomes valid', function() { - var valid = true; - ctrl.$validators.test = function(modelValue, viewValue) { - return valid; - }; - - scope.$apply('value = "abc"'); - expect(scope.value).toBe('abc'); - - valid = false; - ctrl.$validate(); - expect(scope.value).toBeUndefined(); - - valid = true; - ctrl.$validate(); - expect(scope.value).toBe('abc'); - }); - - it('should not update the model when it is valid, but there is a parse error', function() { - ctrl.$parsers.push(function(modelValue) { - return undefined; - }); - - ctrl.$setViewValue('abc'); - expect(ctrl.$error.parse).toBe(true); - expect(scope.value).toBeUndefined(); - - ctrl.$validators.test = function(modelValue, viewValue) { - return true; - }; - - ctrl.$validate(); - expect(ctrl.$error).toEqual({parse: true}); - expect(scope.value).toBeUndefined(); - }); - - it('should not set an invalid model to undefined when validity is the same', function() { - ctrl.$validators.test = function() { - return false; - }; - - scope.$apply('value = "invalid"'); - expect(ctrl.$valid).toBe(false); - expect(scope.value).toBe('invalid'); - - ctrl.$validate(); - expect(ctrl.$valid).toBe(false); - expect(scope.value).toBe('invalid'); - }); - - it('should not change a model that has a formatter', function() { - ctrl.$validators.test = function() { - return true; - }; - - ctrl.$formatters.push(function(modelValue) { - return 'xyz'; - }); - - scope.$apply('value = "abc"'); - expect(ctrl.$viewValue).toBe('xyz'); - - ctrl.$validate(); - expect(scope.value).toBe('abc'); - }); - - it('should not change a model that has a parser', function() { - ctrl.$validators.test = function() { - return true; - }; - - ctrl.$parsers.push(function(modelValue) { - return 'xyz'; - }); - - scope.$apply('value = "abc"'); - - ctrl.$validate(); - expect(scope.value).toBe('abc'); - }); - }); - - describe('view -> model update', function() { - it('should always perform validations using the parsed model value', function() { - var captures; - ctrl.$validators.raw = function() { - captures = arguments; - return captures[0]; - }; - - ctrl.$parsers.push(function(value) { - return value.toUpperCase(); - }); - - ctrl.$setViewValue('my-value'); - - expect(captures).toEqual(['MY-VALUE', 'my-value']); - }); - - it('should always perform validations using the formatted view value', function() { - var captures; - ctrl.$validators.raw = function() { - captures = arguments; - return captures[0]; - }; - - ctrl.$formatters.push(function(value) { - return value + '...'; - }); - - scope.$apply('value = "matias"'); - - expect(captures).toEqual(['matias', 'matias...']); - }); - - it('should only perform validations if the view value is different', function() { - var count = 0; - ctrl.$validators.countMe = function() { - count++; - }; - - ctrl.$setViewValue('my-value'); - expect(count).toBe(1); - - ctrl.$setViewValue('my-value'); - expect(count).toBe(1); - - ctrl.$setViewValue('your-value'); - expect(count).toBe(2); - }); - }); - - it('should perform validations twice each time the model value changes within a digest', function() { - var count = 0; - ctrl.$validators.number = function(value) { - count++; - return (/^\d+$/).test(value); - }; - - scope.$apply('value = ""'); - expect(count).toBe(1); - - scope.$apply('value = 1'); - expect(count).toBe(2); - - scope.$apply('value = 1'); - expect(count).toBe(2); - - scope.$apply('value = ""'); - expect(count).toBe(3); - }); - - it('should only validate to true if all validations are true', function() { - var curry = function(v) { - return function() { - return v; - }; - }; - - ctrl.$modelValue = undefined; - ctrl.$validators.a = curry(true); - ctrl.$validators.b = curry(true); - ctrl.$validators.c = curry(false); - - ctrl.$validate(); - expect(ctrl.$valid).toBe(false); - - ctrl.$validators.c = curry(true); - - ctrl.$validate(); - expect(ctrl.$valid).toBe(true); - }); - - it('should register invalid validations on the $error object', function() { - var curry = function(v) { - return function() { - return v; - }; - }; - - ctrl.$modelValue = undefined; - ctrl.$validators.unique = curry(false); - ctrl.$validators.tooLong = curry(false); - ctrl.$validators.notNumeric = curry(true); - - ctrl.$validate(); - - expect(ctrl.$error.unique).toBe(true); - expect(ctrl.$error.tooLong).toBe(true); - expect(ctrl.$error.notNumeric).not.toBe(true); - }); - - it('should render a validator asynchronously when a promise is returned', inject(function($q) { - var defer; - ctrl.$asyncValidators.promiseValidator = function(value) { - defer = $q.defer(); - return defer.promise; - }; - - scope.$apply('value = ""'); - - expect(ctrl.$valid).toBeUndefined(); - expect(ctrl.$invalid).toBeUndefined(); - expect(ctrl.$pending.promiseValidator).toBe(true); - - defer.resolve(); - scope.$digest(); - - expect(ctrl.$valid).toBe(true); - expect(ctrl.$invalid).toBe(false); - expect(ctrl.$pending).toBeUndefined(); - - scope.$apply('value = "123"'); - - defer.reject(); - scope.$digest(); - - expect(ctrl.$valid).toBe(false); - expect(ctrl.$invalid).toBe(true); - expect(ctrl.$pending).toBeUndefined(); - })); - - it('should throw an error when a promise is not returned for an asynchronous validator', inject(function($q) { - ctrl.$asyncValidators.async = function(value) { - return true; - }; - - expect(function() { - scope.$apply('value = "123"'); - }).toThrowMinErr("ngModel", "$asyncValidators", - "Expected asynchronous validator to return a promise but got 'true' instead."); - })); - - it('should only run the async validators once all the sync validators have passed', - inject(function($q) { - - var stages = {}; - - stages.sync = { status1: false, status2: false, count: 0 }; - ctrl.$validators.syncValidator1 = function(modelValue, viewValue) { - stages.sync.count++; - return stages.sync.status1; - }; - - ctrl.$validators.syncValidator2 = function(modelValue, viewValue) { - stages.sync.count++; - return stages.sync.status2; - }; - - stages.async = { defer: null, count: 0 }; - ctrl.$asyncValidators.asyncValidator = function(modelValue, viewValue) { - stages.async.defer = $q.defer(); - stages.async.count++; - return stages.async.defer.promise; - }; - - scope.$apply('value = "123"'); - - expect(ctrl.$valid).toBe(false); - expect(ctrl.$invalid).toBe(true); - - expect(stages.sync.count).toBe(2); - expect(stages.async.count).toBe(0); - - stages.sync.status1 = true; - - scope.$apply('value = "456"'); - - expect(stages.sync.count).toBe(4); - expect(stages.async.count).toBe(0); - - stages.sync.status2 = true; - - scope.$apply('value = "789"'); - - expect(stages.sync.count).toBe(6); - expect(stages.async.count).toBe(1); - - stages.async.defer.resolve(); - scope.$apply(); - - expect(ctrl.$valid).toBe(true); - expect(ctrl.$invalid).toBe(false); - })); - - it('should ignore expired async validation promises once delivered', inject(function($q) { - var defer, oldDefer, newDefer; - ctrl.$asyncValidators.async = function(value) { - defer = $q.defer(); - return defer.promise; - }; - - scope.$apply('value = ""'); - oldDefer = defer; - scope.$apply('value = "123"'); - newDefer = defer; - - newDefer.reject(); - scope.$digest(); - oldDefer.resolve(); - scope.$digest(); - - expect(ctrl.$valid).toBe(false); - expect(ctrl.$invalid).toBe(true); - expect(ctrl.$pending).toBeUndefined(); - })); - - it('should clear and ignore all pending promises when the model value changes', inject(function($q) { - ctrl.$validators.sync = function(value) { - return true; - }; - - var defers = []; - ctrl.$asyncValidators.async = function(value) { - var defer = $q.defer(); - defers.push(defer); - return defer.promise; - }; - - scope.$apply('value = "123"'); - expect(ctrl.$pending).toEqual({async: true}); - expect(ctrl.$valid).toBe(undefined); - expect(ctrl.$invalid).toBe(undefined); - expect(defers.length).toBe(1); - expect(isObject(ctrl.$pending)).toBe(true); - - scope.$apply('value = "456"'); - expect(ctrl.$pending).toEqual({async: true}); - expect(ctrl.$valid).toBe(undefined); - expect(ctrl.$invalid).toBe(undefined); - expect(defers.length).toBe(2); - expect(isObject(ctrl.$pending)).toBe(true); - - defers[1].resolve(); - scope.$digest(); - expect(ctrl.$valid).toBe(true); - expect(ctrl.$invalid).toBe(false); - expect(isObject(ctrl.$pending)).toBe(false); - })); - - it('should clear and ignore all pending promises when a parser fails', inject(function($q) { - var failParser = false; - ctrl.$parsers.push(function(value) { - return failParser ? undefined : value; - }); - - var defer; - ctrl.$asyncValidators.async = function(value) { - defer = $q.defer(); - return defer.promise; - }; - - ctrl.$setViewValue('x..y..z'); - expect(ctrl.$valid).toBe(undefined); - expect(ctrl.$invalid).toBe(undefined); - - failParser = true; - - ctrl.$setViewValue('1..2..3'); - expect(ctrl.$valid).toBe(false); - expect(ctrl.$invalid).toBe(true); - expect(isObject(ctrl.$pending)).toBe(false); - - defer.resolve(); - scope.$digest(); - - expect(ctrl.$valid).toBe(false); - expect(ctrl.$invalid).toBe(true); - expect(isObject(ctrl.$pending)).toBe(false); - })); - - it('should clear all errors from async validators if a parser fails', inject(function($q) { - var failParser = false; - ctrl.$parsers.push(function(value) { - return failParser ? undefined : value; - }); - - ctrl.$asyncValidators.async = function(value) { - return $q.reject(); - }; - - ctrl.$setViewValue('x..y..z'); - expect(ctrl.$error).toEqual({async: true}); - - failParser = true; - - ctrl.$setViewValue('1..2..3'); - expect(ctrl.$error).toEqual({parse: true}); - })); - - it('should clear all errors from async validators if a sync validator fails', inject(function($q) { - var failValidator = false; - ctrl.$validators.sync = function(value) { - return !failValidator; - }; - - ctrl.$asyncValidators.async = function(value) { - return $q.reject(); - }; - - ctrl.$setViewValue('x..y..z'); - expect(ctrl.$error).toEqual({async: true}); - - failValidator = true; - - ctrl.$setViewValue('1..2..3'); - expect(ctrl.$error).toEqual({sync: true}); - })); - - it('should re-evaluate the form validity state once the asynchronous promise has been delivered', - inject(function($compile, $rootScope, $q) { - - var element = $compile('
' + - '' + - '' + - '
')($rootScope); - var inputElm = element.find('input'); - - var formCtrl = $rootScope.myForm; - var usernameCtrl = formCtrl.username; - var ageCtrl = formCtrl.age; - - var usernameDefer; - usernameCtrl.$asyncValidators.usernameAvailability = function() { - usernameDefer = $q.defer(); - return usernameDefer.promise; - }; - - $rootScope.$digest(); - expect(usernameCtrl.$invalid).toBe(true); - expect(formCtrl.$invalid).toBe(true); - - usernameCtrl.$setViewValue('valid-username'); - $rootScope.$digest(); - - expect(formCtrl.$pending.usernameAvailability).toBeTruthy(); - expect(usernameCtrl.$invalid).toBe(undefined); - expect(formCtrl.$invalid).toBe(undefined); - - usernameDefer.resolve(); - $rootScope.$digest(); - expect(usernameCtrl.$invalid).toBe(false); - expect(formCtrl.$invalid).toBe(true); - - ageCtrl.$setViewValue(22); - $rootScope.$digest(); - - expect(usernameCtrl.$invalid).toBe(false); - expect(ageCtrl.$invalid).toBe(false); - expect(formCtrl.$invalid).toBe(false); - - usernameCtrl.$setViewValue('valid'); - $rootScope.$digest(); - - expect(usernameCtrl.$invalid).toBe(true); - expect(ageCtrl.$invalid).toBe(false); - expect(formCtrl.$invalid).toBe(true); - - usernameCtrl.$setViewValue('another-valid-username'); - $rootScope.$digest(); - - usernameDefer.resolve(); - $rootScope.$digest(); - - expect(usernameCtrl.$invalid).toBe(false); - expect(formCtrl.$invalid).toBe(false); - expect(formCtrl.$pending).toBeFalsy(); - expect(ageCtrl.$invalid).toBe(false); - - dealoc(element); - })); - - - it('should minimize janky setting of classes during $validate() and ngModelWatch', inject(function($animate, $compile, $rootScope) { - var addClass = $animate.$$addClassImmediately; - var removeClass = $animate.$$removeClassImmediately; - var addClassCallCount = 0; - var removeClassCallCount = 0; - var input; - $animate.$$addClassImmediately = function(element, className) { - if (input && element[0] === input[0]) ++addClassCallCount; - return addClass.call($animate, element, className); - }; - - $animate.$$removeClassImmediately = function(element, className) { - if (input && element[0] === input[0]) ++removeClassCallCount; - return removeClass.call($animate, element, className); - }; - - dealoc(element); - - $rootScope.value = "123456789"; - element = $compile( - '
' + - '' + - '
' - )($rootScope); - - var form = $rootScope.form; - input = element.children().eq(0); - - $rootScope.$digest(); - - expect(input).toBeValid(); - expect(input).not.toHaveClass('ng-invalid-maxlength'); - expect(input).toHaveClass('ng-valid-maxlength'); - expect(addClassCallCount).toBe(1); - expect(removeClassCallCount).toBe(0); - - dealoc(element); - })); - - it('should always use the most recent $viewValue for validation', function() { - ctrl.$parsers.push(function(value) { - if (value && value.substr(-1) === 'b') { - value = 'a'; - ctrl.$setViewValue(value); - ctrl.$render(); - } - - return value; - }); - - ctrl.$validators.mock = function(modelValue) { - return true; - }; - - spyOn(ctrl.$validators, 'mock').andCallThrough(); - - ctrl.$setViewValue('ab'); - - expect(ctrl.$validators.mock).toHaveBeenCalledWith('a', 'a'); - expect(ctrl.$validators.mock.calls.length).toEqual(2); - }); - - it('should validate even if the modelValue did not change', function() { - ctrl.$parsers.push(function(value) { - if (value && value.substr(-1) === 'b') { - value = 'a'; - } - - return value; - }); - - ctrl.$validators.mock = function(modelValue) { - return true; - }; - - spyOn(ctrl.$validators, 'mock').andCallThrough(); - - ctrl.$setViewValue('a'); - - expect(ctrl.$validators.mock).toHaveBeenCalledWith('a', 'a'); - expect(ctrl.$validators.mock.calls.length).toEqual(1); - - ctrl.$setViewValue('ab'); - - expect(ctrl.$validators.mock).toHaveBeenCalledWith('a', 'ab'); - expect(ctrl.$validators.mock.calls.length).toEqual(2); - }); - - }); -}); - - -describe('ngModel', function() { - var EMAIL_REGEXP = /^[a-z0-9!#$%&'*+\/=?^_`{|}~.-]+@[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i; - - it('should set css classes (ng-valid, ng-invalid, ng-pristine, ng-dirty, ng-untouched, ng-touched)', - inject(function($compile, $rootScope, $sniffer) { - var element = $compile('')($rootScope); - - $rootScope.$digest(); - expect(element).toBeValid(); - expect(element).toBePristine(); - expect(element).toBeUntouched(); - expect(element.hasClass('ng-valid-email')).toBe(true); - expect(element.hasClass('ng-invalid-email')).toBe(false); - - $rootScope.$apply("value = 'invalid-email'"); - expect(element).toBeInvalid(); - expect(element).toBePristine(); - expect(element.hasClass('ng-valid-email')).toBe(false); - expect(element.hasClass('ng-invalid-email')).toBe(true); - - element.val('invalid-again'); - browserTrigger(element, ($sniffer.hasEvent('input')) ? 'input' : 'change'); - expect(element).toBeInvalid(); - expect(element).toBeDirty(); - expect(element.hasClass('ng-valid-email')).toBe(false); - expect(element.hasClass('ng-invalid-email')).toBe(true); - - element.val('vojta@google.com'); - browserTrigger(element, $sniffer.hasEvent('input') ? 'input' : 'change'); - expect(element).toBeValid(); - expect(element).toBeDirty(); - expect(element.hasClass('ng-valid-email')).toBe(true); - expect(element.hasClass('ng-invalid-email')).toBe(false); - - browserTrigger(element, 'blur'); - expect(element).toBeTouched(); - - dealoc(element); - })); - - - it('should set invalid classes on init', inject(function($compile, $rootScope) { - var element = $compile('')($rootScope); - $rootScope.$digest(); - - expect(element).toBeInvalid(); - expect(element).toHaveClass('ng-invalid-required'); - - dealoc(element); - })); - - describe('custom formatter and parser that are added by a directive in post linking', function() { - var inputElm, scope; - beforeEach(module(function($compileProvider) { - $compileProvider.directive('customFormat', function() { - return { - require: 'ngModel', - link: function(scope, element, attrs, ngModelCtrl) { - ngModelCtrl.$formatters.push(function(value) { - return value.part; - }); - ngModelCtrl.$parsers.push(function(value) { - return {part: value}; - }); - } - }; - }); - })); - - afterEach(function() { - dealoc(inputElm); - }); - - function createInput(type) { - inject(function($compile, $rootScope) { - scope = $rootScope; - inputElm = $compile('')($rootScope); - }); - } - - it('should use them after the builtin ones for text inputs', function() { - createInput('text'); - scope.$apply('val = {part: "a"}'); - expect(inputElm.val()).toBe('a'); - - inputElm.val('b'); - browserTrigger(inputElm, 'change'); - expect(scope.val).toEqual({part: 'b'}); - }); - - it('should use them after the builtin ones for number inputs', function() { - createInput('number'); - scope.$apply('val = {part: 1}'); - expect(inputElm.val()).toBe('1'); - - inputElm.val('2'); - browserTrigger(inputElm, 'change'); - expect(scope.val).toEqual({part: 2}); - }); - - it('should use them after the builtin ones for date inputs', function() { - createInput('date'); - scope.$apply(function() { - scope.val = {part: new Date(2000, 10, 8)}; - }); - expect(inputElm.val()).toBe('2000-11-08'); - - inputElm.val('2001-12-09'); - browserTrigger(inputElm, 'change'); - expect(scope.val).toEqual({part: new Date(2001, 11, 9)}); - }); - }); - - - it('should always format the viewValue as a string for a blank input type when the value is present', - inject(function($compile, $rootScope, $sniffer) { - - var form = $compile('
')($rootScope); - - $rootScope.val = 123; - $rootScope.$digest(); - expect($rootScope.form.field.$viewValue).toBe('123'); - - $rootScope.val = null; - $rootScope.$digest(); - expect($rootScope.form.field.$viewValue).toBe(null); - - dealoc(form); - })); - - it('should always format the viewValue as a string for a `text` input type when the value is present', - inject(function($compile, $rootScope, $sniffer) { - - var form = $compile('
')($rootScope); - $rootScope.val = 123; - $rootScope.$digest(); - expect($rootScope.form.field.$viewValue).toBe('123'); - - $rootScope.val = null; - $rootScope.$digest(); - expect($rootScope.form.field.$viewValue).toBe(null); - - dealoc(form); - })); - - it('should always format the viewValue as a string for an `email` input type when the value is present', - inject(function($compile, $rootScope, $sniffer) { - - var form = $compile('
')($rootScope); - $rootScope.val = 123; - $rootScope.$digest(); - expect($rootScope.form.field.$viewValue).toBe('123'); - - $rootScope.val = null; - $rootScope.$digest(); - expect($rootScope.form.field.$viewValue).toBe(null); - - dealoc(form); - })); - - it('should always format the viewValue as a string for a `url` input type when the value is present', - inject(function($compile, $rootScope, $sniffer) { - - var form = $compile('
')($rootScope); - $rootScope.val = 123; - $rootScope.$digest(); - expect($rootScope.form.field.$viewValue).toBe('123'); - - $rootScope.val = null; - $rootScope.$digest(); - expect($rootScope.form.field.$viewValue).toBe(null); - - dealoc(form); - })); - - it('should set the control touched state on "blur" event', inject(function($compile, $rootScope) { - var element = $compile('
' + - '' + - '
')($rootScope); - var inputElm = element.find('input'); - var control = $rootScope.myForm.myControl; - - expect(control.$touched).toBe(false); - expect(control.$untouched).toBe(true); - - browserTrigger(inputElm, 'blur'); - expect(control.$touched).toBe(true); - expect(control.$untouched).toBe(false); - - dealoc(element); - })); - - it('should not cause a digest on "blur" event if control is already touched', - inject(function($compile, $rootScope) { - - var element = $compile('
' + - '' + - '
')($rootScope); - var inputElm = element.find('input'); - var control = $rootScope.myForm.myControl; - - control.$setTouched(); - spyOn($rootScope, '$apply'); - browserTrigger(inputElm, 'blur'); - - expect($rootScope.$apply).not.toHaveBeenCalled(); - - dealoc(element); - })); - - it('should digest asynchronously on "blur" event if a apply is already in progress', - inject(function($compile, $rootScope) { - - var element = $compile('
' + - '' + - '
')($rootScope); - var inputElm = element.find('input'); - var control = $rootScope.myForm.myControl; - - $rootScope.$apply(function() { - expect(control.$touched).toBe(false); - expect(control.$untouched).toBe(true); - - browserTrigger(inputElm, 'blur'); - - expect(control.$touched).toBe(false); - expect(control.$untouched).toBe(true); - }); - - expect(control.$touched).toBe(true); - expect(control.$untouched).toBe(false); - - dealoc(element); - })); - - - it('should register/deregister a nested ngModel with parent form when entering or leaving DOM', - inject(function($compile, $rootScope) { - - var element = $compile('
' + - '' + - '
')($rootScope); - var isFormValid; - - $rootScope.inputPresent = false; - $rootScope.$watch('myForm.$valid', function(value) { isFormValid = value; }); - - $rootScope.$apply(); - - expect($rootScope.myForm.$valid).toBe(true); - expect(isFormValid).toBe(true); - expect($rootScope.myForm.myControl).toBeUndefined(); - - $rootScope.inputPresent = true; - $rootScope.$apply(); - - expect($rootScope.myForm.$valid).toBe(false); - expect(isFormValid).toBe(false); - expect($rootScope.myForm.myControl).toBeDefined(); - - $rootScope.inputPresent = false; - $rootScope.$apply(); - - expect($rootScope.myForm.$valid).toBe(true); - expect(isFormValid).toBe(true); - expect($rootScope.myForm.myControl).toBeUndefined(); - - dealoc(element); - })); - - - it('should register/deregister a nested ngModel with parent form when entering or leaving DOM with animations', - function() { - - // ngAnimate performs the dom manipulation after digest, and since the form validity can be affected by a form - // control going away we must ensure that the deregistration happens during the digest while we are still doing - // dirty checking. - module('ngAnimate'); - - inject(function($compile, $rootScope) { - var element = $compile('
' + - '' + - '
')($rootScope); - var isFormValid; - - $rootScope.inputPresent = false; - // this watch ensure that the form validity gets updated during digest (so that we can observe it) - $rootScope.$watch('myForm.$valid', function(value) { isFormValid = value; }); - - $rootScope.$apply(); - - expect($rootScope.myForm.$valid).toBe(true); - expect(isFormValid).toBe(true); - expect($rootScope.myForm.myControl).toBeUndefined(); - - $rootScope.inputPresent = true; - $rootScope.$apply(); - - expect($rootScope.myForm.$valid).toBe(false); - expect(isFormValid).toBe(false); - expect($rootScope.myForm.myControl).toBeDefined(); - - $rootScope.inputPresent = false; - $rootScope.$apply(); - - expect($rootScope.myForm.$valid).toBe(true); - expect(isFormValid).toBe(true); - expect($rootScope.myForm.myControl).toBeUndefined(); - - dealoc(element); - }); - }); - - it('should keep previously defined watches consistent when changes in validity are made', - inject(function($compile, $rootScope) { - - var isFormValid; - $rootScope.$watch('myForm.$valid', function(value) { isFormValid = value; }); - - var element = $compile('
' + - '' + - '
')($rootScope); - - $rootScope.$apply(); - expect(isFormValid).toBe(false); - expect($rootScope.myForm.$valid).toBe(false); - - $rootScope.value='value'; - $rootScope.$apply(); - expect(isFormValid).toBe(true); - expect($rootScope.myForm.$valid).toBe(true); - - dealoc(element); - })); - -}); - +/* globals getInputCompileHelper: false */ describe('input', function() { - var formElm, inputElm, scope, $compile, $sniffer, $browser, changeInputValueTo, currentSpec; + var helper, $compile, $rootScope, $browser, $sniffer, $timeout, $q; - function compileInput(inputHtml, mockValidity) { - inputElm = jqLite(inputHtml); - if (isObject(mockValidity)) { - VALIDITY_STATE_PROPERTY = 'ngMockValidity'; - inputElm.prop(VALIDITY_STATE_PROPERTY, mockValidity); - currentSpec.after(function() { - VALIDITY_STATE_PROPERTY = 'validity'; - }); - } - formElm = jqLite('
'); - formElm.append(inputElm); - $compile(formElm)(scope); - scope.$digest(); - } - - var attrs; - beforeEach(function() { currentSpec = this; }); - afterEach(function() { currentSpec = null; }); - beforeEach(module(function($compileProvider) { - $compileProvider.directive('attrCapture', function() { - return function(scope, element, $attrs) { - attrs = $attrs; - }; - }); - })); - - beforeEach(inject(function($injector, _$sniffer_, _$browser_) { - $sniffer = _$sniffer_; - $browser = _$browser_; - $compile = $injector.get('$compile'); - scope = $injector.get('$rootScope'); - - changeInputValueTo = function(value) { - inputElm.val(value); - browserTrigger(inputElm, $sniffer.hasEvent('input') ? 'input' : 'change'); - }; - })); + beforeEach(function() { + helper = getInputCompileHelper(this); + }); afterEach(function() { - dealoc(formElm); + helper.dealoc(); }); + beforeEach(inject(function(_$compile_, _$rootScope_, _$browser_, _$sniffer_, _$timeout_, _$q_) { + $compile = _$compile_; + $rootScope = _$rootScope_; + $browser = _$browser_; + $sniffer = _$sniffer_; + $timeout = _$timeout_; + $q = _$q_; + })); + + it('should bind to a model', function() { - compileInput(''); + var inputElm = helper.compileInput(''); - scope.$apply("name = 'misko'"); + $rootScope.$apply("name = 'misko'"); expect(inputElm.val()).toBe('misko'); }); @@ -1526,7 +46,7 @@ describe('input', function() { } }); - compileInput(''); + var inputElm = helper.compileInput(''); expect(inputElm.prop('readOnly')).toBe(false); expect(inputElm.prop('disabled')).toBe(false); @@ -1537,67 +57,63 @@ describe('input', function() { it('should update the model on "blur" event', function() { - compileInput(''); + var inputElm = helper.compileInput(''); - changeInputValueTo('adam'); - expect(scope.name).toEqual('adam'); + helper.changeInputValueTo('adam'); + expect($rootScope.name).toEqual('adam'); }); it('should not add the property to the scope if name is unspecified', function() { - inputElm = jqLite(''); - formElm = jqLite('
'); - formElm.append(inputElm); - $compile(formElm)(scope); + helper.compileInput(''); - spyOn(scope.form, '$addControl').andCallThrough(); - spyOn(scope.form, '$$renameControl').andCallThrough(); - - scope.$digest(); - - expect(scope.form['undefined']).toBeUndefined(); - expect(scope.form.$addControl).not.toHaveBeenCalled(); - expect(scope.form.$$renameControl).not.toHaveBeenCalled(); + expect($rootScope.form['undefined']).toBeUndefined(); + expect($rootScope.form.$addControl).not.toHaveBeenCalled(); + expect($rootScope.form.$$renameControl).not.toHaveBeenCalled(); }); describe('compositionevents', function() { - it('should not update the model between "compositionstart" and "compositionend" on non android', inject(function($sniffer) { + it('should not update the model between "compositionstart" and "compositionend" on non android', function() { + $sniffer.android = false; - compileInput(''); - changeInputValueTo('a'); - expect(scope.name).toEqual('a'); + var inputElm = helper.compileInput(''); + helper.changeInputValueTo('a'); + expect($rootScope.name).toEqual('a'); browserTrigger(inputElm, 'compositionstart'); - changeInputValueTo('adam'); - expect(scope.name).toEqual('a'); + helper.changeInputValueTo('adam'); + expect($rootScope.name).toEqual('a'); browserTrigger(inputElm, 'compositionend'); - changeInputValueTo('adam'); - expect(scope.name).toEqual('adam'); - })); + helper.changeInputValueTo('adam'); + expect($rootScope.name).toEqual('adam'); + }); - it('should update the model between "compositionstart" and "compositionend" on android', inject(function($sniffer) { + + it('should update the model between "compositionstart" and "compositionend" on android', function() { $sniffer.android = true; - compileInput(''); - changeInputValueTo('a'); - expect(scope.name).toEqual('a'); + var inputElm = helper.compileInput(''); + helper.changeInputValueTo('a'); + expect($rootScope.name).toEqual('a'); browserTrigger(inputElm, 'compositionstart'); - changeInputValueTo('adam'); - expect(scope.name).toEqual('adam'); + helper.changeInputValueTo('adam'); + expect($rootScope.name).toEqual('adam'); browserTrigger(inputElm, 'compositionend'); - changeInputValueTo('adam2'); - expect(scope.name).toEqual('adam2'); - })); + helper.changeInputValueTo('adam2'); + expect($rootScope.name).toEqual('adam2'); + }); + + + it('should update the model on "compositionend"', function() { + var inputElm = helper.compileInput(''); + browserTrigger(inputElm, 'compositionstart'); + helper.changeInputValueTo('caitp'); + expect($rootScope.name).toBeUndefined(); + browserTrigger(inputElm, 'compositionend'); + expect($rootScope.name).toEqual('caitp'); + }); }); - it('should update the model on "compositionend"', function() { - compileInput(''); - browserTrigger(inputElm, 'compositionstart'); - changeInputValueTo('caitp'); - expect(scope.name).toBeUndefined(); - browserTrigger(inputElm, 'compositionend'); - expect(scope.name).toEqual('caitp'); - }); describe("IE placeholder input events", function() { //IE fires an input event whenever a placeholder visually changes, essentially treating it as a value @@ -1609,32 +125,33 @@ describe('input', function() { //These tests try simulate various scenerios which do/do-not fire the extra input event it('should not dirty the model on an input event in response to a placeholder change', function() { - compileInput(''); + var inputElm = helper.compileInput(''); msie && browserTrigger(inputElm, 'input'); expect(inputElm.attr('placeholder')).toBe('Test'); expect(inputElm).toBePristine(); - attrs.$set('placeholder', ''); + helper.attrs.$set('placeholder', ''); msie && browserTrigger(inputElm, 'input'); expect(inputElm.attr('placeholder')).toBe(''); expect(inputElm).toBePristine(); - attrs.$set('placeholder', 'Test Again'); + helper.attrs.$set('placeholder', 'Test Again'); msie && browserTrigger(inputElm, 'input'); expect(inputElm.attr('placeholder')).toBe('Test Again'); expect(inputElm).toBePristine(); - attrs.$set('placeholder', undefined); + helper.attrs.$set('placeholder', undefined); msie && browserTrigger(inputElm, 'input'); expect(inputElm.attr('placeholder')).toBe(undefined); expect(inputElm).toBePristine(); - changeInputValueTo('foo'); + helper.changeInputValueTo('foo'); expect(inputElm).toBeDirty(); }); - it('should not dirty the model on an input event in response to a interpolated placeholder change', inject(function($rootScope) { - compileInput(''); + + it('should not dirty the model on an input event in response to a interpolated placeholder change', function() { + var inputElm = helper.compileInput(''); msie && browserTrigger(inputElm, 'input'); expect(inputElm).toBePristine(); @@ -1648,13 +165,14 @@ describe('input', function() { msie && browserTrigger(inputElm, 'input'); expect(inputElm).toBePristine(); - changeInputValueTo('foo'); + helper.changeInputValueTo('foo'); expect(inputElm).toBeDirty(); - })); + }); - it('should dirty the model on an input event while in focus even if the placeholder changes', inject(function($rootScope) { + + it('should dirty the model on an input event while in focus even if the placeholder changes', function() { $rootScope.ph = 'Test'; - compileInput(''); + var inputElm = helper.compileInput(''); expect(inputElm).toBePristine(); browserTrigger(inputElm, 'focusin'); @@ -1667,12 +185,13 @@ describe('input', function() { $rootScope.$digest(); expect(inputElm).toBePristine(); - changeInputValueTo('foo'); + helper.changeInputValueTo('foo'); expect(inputElm).toBeDirty(); - })); + }); - it('should not dirty the model on an input event in response to a ng-attr-placeholder change', inject(function($rootScope) { - compileInput(''); + + it('should not dirty the model on an input event in response to a ng-attr-placeholder change', function() { + var inputElm = helper.compileInput(''); expect(inputElm).toBePristine(); $rootScope.ph = 1; @@ -1685,12 +204,13 @@ describe('input', function() { msie && browserTrigger(inputElm, 'input'); expect(inputElm).toBePristine(); - changeInputValueTo('foo'); + helper.changeInputValueTo('foo'); expect(inputElm).toBeDirty(); - })); + }); - it('should not dirty the model on an input event in response to a focus', inject(function($sniffer) { - compileInput(''); + + it('should not dirty the model on an input event in response to a focus', function() { + var inputElm = helper.compileInput(''); msie && browserTrigger(inputElm, 'input'); expect(inputElm.attr('placeholder')).toBe('Test'); expect(inputElm).toBePristine(); @@ -1701,12 +221,13 @@ describe('input', function() { expect(inputElm.attr('placeholder')).toBe('Test'); expect(inputElm).toBePristine(); - changeInputValueTo('foo'); + helper.changeInputValueTo('foo'); expect(inputElm).toBeDirty(); - })); + }); - it('should not dirty the model on an input event in response to a blur', inject(function($sniffer) { - compileInput(''); + + it('should not dirty the model on an input event in response to a blur', function() { + var inputElm = helper.compileInput(''); msie && browserTrigger(inputElm, 'input'); expect(inputElm.attr('placeholder')).toBe('Test'); expect(inputElm).toBePristine(); @@ -1721,35 +242,38 @@ describe('input', function() { browserTrigger(inputElm, 'blur'); expect(inputElm).toBePristine(); - changeInputValueTo('foo'); + helper.changeInputValueTo('foo'); expect(inputElm).toBeDirty(); - })); + }); - it('should dirty the model on an input event if there is a placeholder and value', inject(function($rootScope) { + + it('should dirty the model on an input event if there is a placeholder and value', function() { $rootScope.name = 'foo'; - compileInput(''); + var inputElm = helper.compileInput(''); expect(inputElm.val()).toBe($rootScope.name); expect(inputElm).toBePristine(); - changeInputValueTo('bar'); + helper.changeInputValueTo('bar'); expect(inputElm).toBeDirty(); - })); + }); - it('should dirty the model on an input event if there is a placeholder and value after focusing', inject(function($rootScope) { + + it('should dirty the model on an input event if there is a placeholder and value after focusing', function() { $rootScope.name = 'foo'; - compileInput(''); + var inputElm = helper.compileInput(''); expect(inputElm.val()).toBe($rootScope.name); expect(inputElm).toBePristine(); browserTrigger(inputElm, 'focusin'); browserTrigger(inputElm, 'focus'); - changeInputValueTo('bar'); + helper.changeInputValueTo('bar'); expect(inputElm).toBeDirty(); - })); + }); - it('should dirty the model on an input event if there is a placeholder and value after bluring', inject(function($rootScope) { + + it('should dirty the model on an input event if there is a placeholder and value after bluring', function() { $rootScope.name = 'foo'; - compileInput(''); + var inputElm = helper.compileInput(''); expect(inputElm.val()).toBe($rootScope.name); expect(inputElm).toBePristine(); @@ -1759,96 +283,105 @@ describe('input', function() { browserTrigger(inputElm, 'focusout'); browserTrigger(inputElm, 'blur'); - changeInputValueTo('bar'); + helper.changeInputValueTo('bar'); expect(inputElm).toBeDirty(); - })); + }); }); - it('should interpolate input names', function() { - scope.nameID = '47'; - compileInput(''); - expect(scope.form.name47.$pristine).toBeTruthy(); - changeInputValueTo('caitp'); - expect(scope.form.name47.$dirty).toBeTruthy(); + describe('interpolated names', function() { + + it('should interpolate input names', function() { + $rootScope.nameID = '47'; + var inputElm = helper.compileInput(''); + expect($rootScope.form.name47.$pristine).toBeTruthy(); + helper.changeInputValueTo('caitp'); + expect($rootScope.form.name47.$dirty).toBeTruthy(); + }); + + + it('should rename form controls in form when interpolated name changes', function() { + $rootScope.nameID = "A"; + var inputElm = helper.compileInput(''); + expect($rootScope.form.nameA.$name).toBe('nameA'); + var oldModel = $rootScope.form.nameA; + $rootScope.nameID = "B"; + $rootScope.$digest(); + expect($rootScope.form.nameA).toBeUndefined(); + expect($rootScope.form.nameB).toBe(oldModel); + expect($rootScope.form.nameB.$name).toBe('nameB'); + }); + + + it('should rename form controls in null form when interpolated name changes', function() { + $rootScope.nameID = "A"; + var inputElm = helper.compileInput(''); + var model = inputElm.controller('ngModel'); + expect(model.$name).toBe('nameA'); + + $rootScope.nameID = "B"; + $rootScope.$digest(); + expect(model.$name).toBe('nameB'); + }); }); - - it('should rename form controls in form when interpolated name changes', function() { - scope.nameID = "A"; - compileInput(''); - expect(scope.form.nameA.$name).toBe('nameA'); - var oldModel = scope.form.nameA; - scope.nameID = "B"; - scope.$digest(); - expect(scope.form.nameA).toBeUndefined(); - expect(scope.form.nameB).toBe(oldModel); - expect(scope.form.nameB.$name).toBe('nameB'); - }); - - - it('should rename form controls in null form when interpolated name changes', function() { - var element = $compile('')(scope); - scope.nameID = "A"; - scope.$digest(); - var model = element.controller('ngModel'); - expect(model.$name).toBe('nameA'); - - scope.nameID = "B"; - scope.$digest(); - expect(model.$name).toBe('nameB'); - }); - - describe('"change" event', function() { - function assertBrowserSupportsChangeEvent(inputEventSupported) { - // Force browser to report a lack of an 'input' event - $sniffer.hasEvent = function(eventName) { - if (eventName === 'input' && !inputEventSupported) { - return false; - } - return true; - }; - compileInput(''); + var assertBrowserSupportsChangeEvent; + + beforeEach(function() { + assertBrowserSupportsChangeEvent = function(inputEventSupported) { + // Force browser to report a lack of an 'input' event + $sniffer.hasEvent = function(eventName) { + return !(eventName === 'input' && !inputEventSupported); + }; + var inputElm = helper.compileInput(''); + + inputElm.val('mark'); + browserTrigger(inputElm, 'change'); + expect($rootScope.name).toEqual('mark'); + }; + }); - inputElm.val('mark'); - browserTrigger(inputElm, 'change'); - expect(scope.name).toEqual('mark'); - } it('should update the model event if the browser does not support the "input" event',function() { assertBrowserSupportsChangeEvent(false); }); + it('should update the model event if the browser supports the "input" ' + 'event so that form auto complete works',function() { assertBrowserSupportsChangeEvent(true); }); + if (!_jqLiteMode) { describe('double $digest when triggering an event using jQuery', function() { - function run() { - $sniffer.hasEvent = function(eventName) { - return eventName !== 'input'; + var run; + + beforeEach(function() { + run = function(scope) { + + $sniffer.hasEvent = function(eventName) { return eventName !== 'input'; }; + + scope = scope || $rootScope; + + var inputElm = helper.compileInput('', false, scope); + + scope.field = 'fake field'; + scope.$watch('field', function() { + // We need to use _originalTrigger since trigger is modified by Angular Scenario. + inputElm._originalTrigger('change'); + }); + scope.$apply(); }; - - compileInput(''); - - scope.field = 'fake field'; - scope.$watch('field', function() { - // We need to use _originalTrigger since trigger is modified by Angular Scenario. - inputElm._originalTrigger('change'); - }); - scope.$apply(); - } + }); it('should not cause the double $digest with non isolate scopes', function() { run(); }); it('should not cause the double $digest with isolate scopes', function() { - scope = scope.$new(true); - run(); + run($rootScope.$new(true)); }); }); } @@ -1862,8 +395,9 @@ describe('input', function() { }; }); + it('should update the model on "paste" event if the input value changes', function() { - compileInput(''); + var inputElm = helper.compileInput(''); browserTrigger(inputElm, 'keydown'); $browser.defer.flush(); @@ -1872,20 +406,22 @@ describe('input', function() { inputElm.val('mark'); browserTrigger(inputElm, 'paste'); $browser.defer.flush(); - expect(scope.name).toEqual('mark'); + expect($rootScope.name).toEqual('mark'); }); + it('should update the model on "cut" event', function() { - compileInput(''); + var inputElm = helper.compileInput(''); inputElm.val('john'); browserTrigger(inputElm, 'cut'); $browser.defer.flush(); - expect(scope.name).toEqual('john'); + expect($rootScope.name).toEqual('john'); }); + it('should cancel the delayed dirty if a change occurs', function() { - compileInput(''); + var inputElm = helper.compileInput(''); var ctrl = inputElm.controller('ngModel'); browserTrigger(inputElm, 'keydown', {target: inputElm[0]}); @@ -1894,7 +430,7 @@ describe('input', function() { expect(inputElm).toBeDirty(); ctrl.$setPristine(); - scope.$apply(); + $rootScope.$apply(); $browser.defer.flush(); expect(inputElm).toBePristine(); @@ -1902,579 +438,37 @@ describe('input', function() { }); - it('should update the model and trim the value', function() { - compileInput(''); + describe('ngTrim', function() { - changeInputValueTo(' a '); - expect(scope.name).toEqual('a'); - }); + it('should update the model and trim the value', function() { + var inputElm = helper.compileInput(''); - - it('should update the model and not trim the value', function() { - compileInput(''); - - changeInputValueTo(' a '); - expect(scope.name).toEqual(' a '); - }); - - - describe('ngModelOptions attributes', function() { - - it('should allow overriding the model update trigger event on text inputs', function() { - compileInput( - ''); - - changeInputValueTo('a'); - expect(scope.name).toBeUndefined(); - browserTrigger(inputElm, 'blur'); - expect(scope.name).toEqual('a'); - }); - - it('should not dirty the input if nothing was changed before updateOn trigger', function() { - compileInput( - ''); - - browserTrigger(inputElm, 'blur'); - expect(scope.form.alias.$pristine).toBeTruthy(); - }); - - it('should allow overriding the model update trigger event on text areas', function() { - compileInput( - ''); - inputElm = formElm.find('textarea'); + var inputElm = helper.compileInput(''); - scope.$apply("name = 'Adam'"); + $rootScope.$apply("name = 'Adam'"); expect(inputElm.val()).toEqual('Adam'); - changeInputValueTo('Shyam'); - expect(scope.name).toEqual('Shyam'); + helper.changeInputValueTo('Shyam'); + expect($rootScope.name).toEqual('Shyam'); - changeInputValueTo('Kai'); - expect(scope.name).toEqual('Kai'); + helper.changeInputValueTo('Kai'); + expect($rootScope.name).toEqual('Kai'); }); it('should ignore textarea without ngModel directive', function() { - compileInput(''); - inputElm = formElm.find('textarea'); + var inputElm = helper.compileInput(''); - changeInputValueTo(''); + helper.changeInputValueTo(''); expect(inputElm.hasClass('ng-valid')).toBe(false); expect(inputElm.hasClass('ng-invalid')).toBe(false); expect(inputElm.hasClass('ng-pristine')).toBe(false); @@ -4830,269 +2488,12 @@ describe('input', function() { }); - describe('ngList', function() { - - it('should parse text into an array', function() { - compileInput(''); - - // model -> view - scope.$apply("list = ['x', 'y', 'z']"); - expect(inputElm.val()).toBe('x, y, z'); - - // view -> model - changeInputValueTo('1, 2, 3'); - expect(scope.list).toEqual(['1', '2', '3']); - }); - - - it("should not clobber text if model changes due to itself", function() { - // When the user types 'a,b' the 'a,' stage parses to ['a'] but if the - // $parseModel function runs it will change to 'a', in essence preventing - // the user from ever typing ','. - compileInput(''); - - changeInputValueTo('a '); - expect(inputElm.val()).toEqual('a '); - expect(scope.list).toEqual(['a']); - - changeInputValueTo('a ,'); - expect(inputElm.val()).toEqual('a ,'); - expect(scope.list).toEqual(['a']); - - changeInputValueTo('a , '); - expect(inputElm.val()).toEqual('a , '); - expect(scope.list).toEqual(['a']); - - changeInputValueTo('a , b'); - expect(inputElm.val()).toEqual('a , b'); - expect(scope.list).toEqual(['a', 'b']); - }); - - - it('should convert empty string to an empty array', function() { - compileInput(''); - - changeInputValueTo(''); - expect(scope.list).toEqual([]); - }); - - it('should be invalid if required and empty', function() { - compileInput(''); - changeInputValueTo(''); - expect(scope.list).toBeUndefined(); - expect(inputElm).toBeInvalid(); - changeInputValueTo('a,b'); - expect(scope.list).toEqual(['a','b']); - expect(inputElm).toBeValid(); - }); - - describe('with a custom separator', function() { - it('should split on the custom separator', function() { - compileInput(''); - - changeInputValueTo('a,a'); - expect(scope.list).toEqual(['a,a']); - - changeInputValueTo('a:b'); - expect(scope.list).toEqual(['a', 'b']); - }); - - - it("should join the list back together with the custom separator", function() { - compileInput(''); - - scope.$apply(function() { - scope.list = ['x', 'y', 'z']; - }); - expect(inputElm.val()).toBe('x : y : z'); - }); - }); - - describe('(with ngTrim undefined or true)', function() { - - it('should ignore separator whitespace when splitting', function() { - compileInput(''); - - changeInputValueTo('a|b'); - expect(scope.list).toEqual(['a', 'b']); - }); - - it('should trim whitespace from each list item', function() { - compileInput(''); - - changeInputValueTo('a | b'); - expect(scope.list).toEqual(['a', 'b']); - }); - }); - - describe('(with ngTrim set to false)', function() { - - it('should use separator whitespace when splitting', function() { - compileInput(''); - - changeInputValueTo('a|b'); - expect(scope.list).toEqual(['a|b']); - - changeInputValueTo('a | b'); - expect(scope.list).toEqual(['a','b']); - - }); - - it("should not trim whitespace from each list item", function() { - compileInput(''); - changeInputValueTo('a | b'); - expect(scope.list).toEqual(['a ',' b']); - }); - - it("should support splitting on newlines", function() { - compileInput('