/
githubmirror
/
angular.js
Обзор
Документация
Войти
/
githubmirror
/
angular.js
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
test/ng/directive/ngModelOptionsSpec.js
940 строк
37 KB
Martin Staffa
fix(ngModelController): allow $overrideModelOptions to set updateOn
05 дек 2017, 22:07
Не верифицирован
05 дек 2017, 22:07
da72477
Код
Авторство
О чём код?
'use strict'; /* globals generateInputCompilerHelper: false, defaultModelOptions: false */ describe('ngModelOptions', function() { describe('defaultModelOptions', function() { it('should provide default values', function() { expect(defaultModelOptions.getOption('updateOn')).toEqual(''); expect(defaultModelOptions.getOption('updateOnDefault')).toEqual(true); expect(defaultModelOptions.getOption('debounce')).toBe(0); expect(defaultModelOptions.getOption('getterSetter')).toBe(false); expect(defaultModelOptions.getOption('allowInvalid')).toBe(false); expect(defaultModelOptions.getOption('timezone')).toBe(null); }); }); describe('directive', function() { describe('basic usage', function() { var helper = {}, $rootScope, $compile, $timeout, $q; generateInputCompilerHelper(helper); beforeEach(inject(function(_$compile_, _$rootScope_, _$timeout_, _$q_) { $compile = _$compile_; $rootScope = _$rootScope_; $timeout = _$timeout_; $q = _$q_; })); describe('should fall back to `defaultModelOptions`', function() { it('if there is no `ngModelOptions` directive', function() { var inputElm = helper.compileInput( '<input type="text" ng-model="name" name="alias" />'); var inputOptions = $rootScope.form.alias.$options; expect(inputOptions.getOption('updateOn')).toEqual(defaultModelOptions.getOption('updateOn')); expect(inputOptions.getOption('updateOnDefault')).toEqual(defaultModelOptions.getOption('updateOnDefault')); expect(inputOptions.getOption('debounce')).toEqual(defaultModelOptions.getOption('debounce')); expect(inputOptions.getOption('getterSetter')).toEqual(defaultModelOptions.getOption('getterSetter')); expect(inputOptions.getOption('allowInvalid')).toEqual(defaultModelOptions.getOption('allowInvalid')); expect(inputOptions.getOption('timezone')).toEqual(defaultModelOptions.getOption('timezone')); }); it('if `ngModelOptions` on the same element does not specify the option', function() { var inputElm = helper.compileInput( '<input type="text" ng-model="name" name="alias" ng-model-options="{ updateOn: \'blur\' }"/>'); var inputOptions = $rootScope.form.alias.$options; expect(inputOptions.getOption('debounce')).toEqual(defaultModelOptions.getOption('debounce')); expect(inputOptions.getOption('updateOnDefault')).toBe(false); expect(inputOptions.getOption('updateOnDefault')).not.toEqual(defaultModelOptions.getOption('updateOnDefault')); }); it('if the first `ngModelOptions` ancestor does not specify the option', function() { var form = $compile('<form name="form" ng-model-options="{ updateOn: \'blur\' }">' + '<input name="alias" ng-model="x">' + '</form>')($rootScope); var inputOptions = $rootScope.form.alias.$options; expect(inputOptions.getOption('debounce')).toEqual(defaultModelOptions.getOption('debounce')); expect(inputOptions.getOption('updateOnDefault')).toBe(false); expect(inputOptions.getOption('updateOnDefault')).not.toEqual(defaultModelOptions.getOption('updateOnDefault')); dealoc(form); }); }); describe('sharing and inheritance', function() { it('should not inherit options from ancestor `ngModelOptions` directives by default', function() { var container = $compile( '<div ng-model-options="{ allowInvalid: true }">' + '<form ng-model-options="{ updateOn: \'blur\' }">' + '<input ng-model-options="{ updateOn: \'default\' }">' + '</form>' + '</div>')($rootScope); var form = container.find('form'); var input = container.find('input'); var containerOptions = container.controller('ngModelOptions').$options; var formOptions = form.controller('ngModelOptions').$options; var inputOptions = input.controller('ngModelOptions').$options; expect(containerOptions.getOption('allowInvalid')).toEqual(true); expect(formOptions.getOption('allowInvalid')).toEqual(false); expect(inputOptions.getOption('allowInvalid')).toEqual(false); expect(containerOptions.getOption('updateOn')).toEqual(''); expect(containerOptions.getOption('updateOnDefault')).toEqual(true); expect(formOptions.getOption('updateOn')).toEqual('blur'); expect(formOptions.getOption('updateOnDefault')).toEqual(false); expect(inputOptions.getOption('updateOn')).toEqual(''); expect(inputOptions.getOption('updateOnDefault')).toEqual(true); dealoc(container); }); it('should inherit options that are marked with "$inherit" from the nearest ancestor `ngModelOptions` directive', function() { var container = $compile( '<div ng-model-options="{ allowInvalid: true }">' + '<form ng-model-options="{ updateOn: \'blur\', allowInvalid: \'$inherit\' }">' + '<input ng-model-options="{ updateOn: \'default\' }">' + '</form>' + '</div>')($rootScope); var form = container.find('form'); var input = container.find('input'); var containerOptions = container.controller('ngModelOptions').$options; var formOptions = form.controller('ngModelOptions').$options; var inputOptions = input.controller('ngModelOptions').$options; expect(containerOptions.getOption('allowInvalid')).toEqual(true); expect(formOptions.getOption('allowInvalid')).toEqual(true); expect(inputOptions.getOption('allowInvalid')).toEqual(false); expect(containerOptions.getOption('updateOn')).toEqual(''); expect(containerOptions.getOption('updateOnDefault')).toEqual(true); expect(formOptions.getOption('updateOn')).toEqual('blur'); expect(formOptions.getOption('updateOnDefault')).toEqual(false); expect(inputOptions.getOption('updateOn')).toEqual(''); expect(inputOptions.getOption('updateOnDefault')).toEqual(true); dealoc(container); }); it('should inherit all unspecified options if the options object contains a `"*"` property with value "$inherit"', function() { var container = $compile( '<div ng-model-options="{ allowInvalid: true, debounce: 100, updateOn: \'keyup\' }">' + '<form ng-model-options="{ updateOn: \'blur\', \'*\': \'$inherit\' }">' + '<input ng-model-options="{ updateOn: \'default\' }">' + '</form>' + '</div>')($rootScope); var form = container.find('form'); var input = container.find('input'); var containerOptions = container.controller('ngModelOptions').$options; var formOptions = form.controller('ngModelOptions').$options; var inputOptions = input.controller('ngModelOptions').$options; expect(containerOptions.getOption('allowInvalid')).toEqual(true); expect(formOptions.getOption('allowInvalid')).toEqual(true); expect(inputOptions.getOption('allowInvalid')).toEqual(false); expect(containerOptions.getOption('debounce')).toEqual(100); expect(formOptions.getOption('debounce')).toEqual(100); expect(inputOptions.getOption('debounce')).toEqual(0); expect(containerOptions.getOption('updateOn')).toEqual('keyup'); expect(containerOptions.getOption('updateOnDefault')).toEqual(false); expect(formOptions.getOption('updateOn')).toEqual('blur'); expect(formOptions.getOption('updateOnDefault')).toEqual(false); expect(inputOptions.getOption('updateOn')).toEqual(''); expect(inputOptions.getOption('updateOnDefault')).toEqual(true); dealoc(container); }); it('should correctly inherit default and another specified event for `updateOn`', function() { var container = $compile( '<div ng-model-options="{updateOn: \'default blur\'}">' + '<input ng-model-options="{\'*\': \'$inherit\'}">' + '</div>')($rootScope); var input = container.find('input'); var inputOptions = input.controller('ngModelOptions').$options; expect(inputOptions.getOption('updateOn')).toEqual('blur'); expect(inputOptions.getOption('updateOnDefault')).toEqual(true); dealoc(container); }); it('should `updateOnDefault` as well if we have `updateOn: "$inherit"`', function() { var container = $compile( '<div ng-model-options="{updateOn: \'keyup\'}">' + '<input ng-model-options="{updateOn: \'$inherit\'}">' + '<div ng-model-options="{updateOn: \'default blur\'}">' + '<input ng-model-options="{updateOn: \'$inherit\'}">' + '</div>' + '</div>')($rootScope); var input1 = container.find('input').eq(0); var inputOptions1 = input1.controller('ngModelOptions').$options; expect(inputOptions1.getOption('updateOn')).toEqual('keyup'); expect(inputOptions1.getOption('updateOnDefault')).toEqual(false); var input2 = container.find('input').eq(1); var inputOptions2 = input2.controller('ngModelOptions').$options; expect(inputOptions2.getOption('updateOn')).toEqual('blur'); expect(inputOptions2.getOption('updateOnDefault')).toEqual(true); dealoc(container); }); it('should make a copy of the options object', function() { $rootScope.options = {updateOn: 'default'}; var inputElm = helper.compileInput( '<input type="text" ng-model="name" name="alias" ' + 'ng-model-options="options"' + '/>'); expect($rootScope.options).toEqual({updateOn: 'default'}); expect($rootScope.form.alias.$options).not.toBe($rootScope.options); }); it('should be retrieved from an ancestor element containing an `ngModelOptions` directive', function() { var doc = $compile( '<form name="test" ' + 'ng-model-options="{ debounce: 10000, updateOn: \'blur\' }" >' + '<input type="text" ng-model="name" name="alias" />' + '</form>')($rootScope); $rootScope.$digest(); var inputElm = doc.find('input'); helper.changeGivenInputTo(inputElm, 'a'); expect($rootScope.name).toEqual(undefined); browserTrigger(inputElm, 'blur'); expect($rootScope.name).toBeUndefined(); $timeout.flush(2000); expect($rootScope.name).toBeUndefined(); $timeout.flush(9000); expect($rootScope.name).toEqual('a'); dealoc(doc); }); it('should allow sharing options between multiple inputs', function() { $rootScope.options = {updateOn: 'default'}; var inputElm = helper.compileInput( '<input type="text" ng-model="name1" name="alias1" ' + 'ng-model-options="options"' + '/>' + '<input type="text" ng-model="name2" name="alias2" ' + 'ng-model-options="options"' + '/>'); helper.changeGivenInputTo(inputElm.eq(0), 'a'); helper.changeGivenInputTo(inputElm.eq(1), 'b'); expect($rootScope.name1).toEqual('a'); expect($rootScope.name2).toEqual('b'); }); }); describe('updateOn', function() { it('should allow overriding the model update trigger event on text inputs', function() { var inputElm = helper.compileInput( '<input type="text" ng-model="name" name="alias" ' + 'ng-model-options="{ updateOn: \'blur\' }"' + '/>'); helper.changeInputValueTo('a'); expect($rootScope.name).toBeUndefined(); browserTrigger(inputElm, 'blur'); expect($rootScope.name).toEqual('a'); }); it('should not dirty the input if nothing was changed before updateOn trigger', function() { var inputElm = helper.compileInput( '<input type="text" ng-model="name" name="alias" ' + 'ng-model-options="{ updateOn: \'blur\' }"' + '/>'); browserTrigger(inputElm, 'blur'); expect($rootScope.form.alias.$pristine).toBeTruthy(); }); it('should allow overriding the model update trigger event on text areas', function() { var inputElm = helper.compileInput( '<textarea ng-model="name" name="alias" ' + 'ng-model-options="{ updateOn: \'blur\' }"' + '/>'); helper.changeInputValueTo('a'); expect($rootScope.name).toBeUndefined(); browserTrigger(inputElm, 'blur'); expect($rootScope.name).toEqual('a'); }); it('should bind the element to a list of events', function() { var inputElm = helper.compileInput( '<input type="text" ng-model="name" name="alias" ' + 'ng-model-options="{ updateOn: \'blur mousemove\' }"' + '/>'); helper.changeInputValueTo('a'); expect($rootScope.name).toBeUndefined(); browserTrigger(inputElm, 'blur'); expect($rootScope.name).toEqual('a'); helper.changeInputValueTo('b'); expect($rootScope.name).toEqual('a'); browserTrigger(inputElm, 'mousemove'); expect($rootScope.name).toEqual('b'); }); it('should allow keeping the default update behavior on text inputs', function() { var inputElm = helper.compileInput( '<input type="text" ng-model="name" name="alias" ' + 'ng-model-options="{ updateOn: \'default\' }"' + '/>'); helper.changeInputValueTo('a'); expect($rootScope.name).toEqual('a'); }); it('should allow overriding the model update trigger event on checkboxes', function() { var inputElm = helper.compileInput( '<input type="checkbox" ng-model="checkbox" ' + 'ng-model-options="{ updateOn: \'blur\' }"' + '/>'); browserTrigger(inputElm, 'click'); expect($rootScope.checkbox).toBeUndefined(); browserTrigger(inputElm, 'blur'); expect($rootScope.checkbox).toBe(true); browserTrigger(inputElm, 'click'); expect($rootScope.checkbox).toBe(true); }); it('should allow keeping the default update behavior on checkboxes', function() { var inputElm = helper.compileInput( '<input type="checkbox" ng-model="checkbox" ' + 'ng-model-options="{ updateOn: \'blur default\' }"' + '/>'); browserTrigger(inputElm, 'click'); expect($rootScope.checkbox).toBe(true); browserTrigger(inputElm, 'click'); expect($rootScope.checkbox).toBe(false); }); it('should allow overriding the model update trigger event on radio buttons', function() { var inputElm = helper.compileInput( '<input type="radio" ng-model="color" value="white" ' + 'ng-model-options="{ updateOn: \'blur\'}"' + '/>' + '<input type="radio" ng-model="color" value="red" ' + 'ng-model-options="{ updateOn: \'blur\'}"' + '/>' + '<input type="radio" ng-model="color" value="blue" ' + 'ng-model-options="{ updateOn: \'blur\'}"' + '/>'); $rootScope.$apply('color = \'white\''); browserTrigger(inputElm[2], 'click'); expect($rootScope.color).toBe('white'); browserTrigger(inputElm[2], 'blur'); expect($rootScope.color).toBe('blue'); }); it('should allow keeping the default update behavior on radio buttons', function() { var inputElm = helper.compileInput( '<input type="radio" ng-model="color" value="white" ' + 'ng-model-options="{ updateOn: \'blur default\' }"' + '/>' + '<input type="radio" ng-model="color" value="red" ' + 'ng-model-options="{ updateOn: \'blur default\' }"' + '/>' + '<input type="radio" ng-model="color" value="blue" ' + 'ng-model-options="{ updateOn: \'blur default\' }"' + '/>'); $rootScope.$apply('color = \'white\''); browserTrigger(inputElm[2], 'click'); expect($rootScope.color).toBe('blue'); }); it('should re-set the trigger events when overridden with $overrideModelOptions', function() { var inputElm = helper.compileInput( '<input type="text" ng-model="name" name="alias" ' + 'ng-model-options="{ updateOn: \'blur click\' }"' + '/>'); var ctrl = inputElm.controller('ngModel'); helper.changeInputValueTo('a'); expect($rootScope.name).toBeUndefined(); browserTrigger(inputElm, 'blur'); expect($rootScope.name).toEqual('a'); helper.changeInputValueTo('b'); expect($rootScope.name).toBe('a'); browserTrigger(inputElm, 'click'); expect($rootScope.name).toEqual('b'); $rootScope.$apply('name = undefined'); expect(inputElm.val()).toBe(''); ctrl.$overrideModelOptions({updateOn: 'blur mousedown'}); helper.changeInputValueTo('a'); expect($rootScope.name).toBeUndefined(); browserTrigger(inputElm, 'blur'); expect($rootScope.name).toEqual('a'); helper.changeInputValueTo('b'); expect($rootScope.name).toBe('a'); browserTrigger(inputElm, 'click'); expect($rootScope.name).toBe('a'); browserTrigger(inputElm, 'mousedown'); expect($rootScope.name).toEqual('b'); }); }); describe('debounce', function() { it('should trigger only after timeout in text inputs', function() { var inputElm = helper.compileInput( '<input type="text" ng-model="name" name="alias" ' + 'ng-model-options="{ debounce: 10000 }"' + '/>'); helper.changeInputValueTo('a'); helper.changeInputValueTo('b'); helper.changeInputValueTo('c'); expect($rootScope.name).toEqual(undefined); $timeout.flush(2000); expect($rootScope.name).toEqual(undefined); $timeout.flush(9000); expect($rootScope.name).toEqual('c'); }); it('should trigger only after timeout in checkboxes', function() { var inputElm = helper.compileInput( '<input type="checkbox" ng-model="checkbox" ' + 'ng-model-options="{ debounce: 10000 }"' + '/>'); browserTrigger(inputElm, 'click'); expect($rootScope.checkbox).toBeUndefined(); $timeout.flush(2000); expect($rootScope.checkbox).toBeUndefined(); $timeout.flush(9000); expect($rootScope.checkbox).toBe(true); }); it('should trigger only after timeout in radio buttons', function() { var inputElm = helper.compileInput( '<input type="radio" ng-model="color" value="white" />' + '<input type="radio" ng-model="color" value="red" ' + 'ng-model-options="{ debounce: 20000 }"' + '/>' + '<input type="radio" ng-model="color" value="blue" ' + 'ng-model-options="{ debounce: 30000 }"' + '/>'); browserTrigger(inputElm[0], 'click'); expect($rootScope.color).toBe('white'); browserTrigger(inputElm[1], 'click'); expect($rootScope.color).toBe('white'); $timeout.flush(12000); expect($rootScope.color).toBe('white'); $timeout.flush(10000); expect($rootScope.color).toBe('red'); }); it('should not trigger digest while debouncing', function() { var inputElm = helper.compileInput( '<input type="text" ng-model="name" name="alias" ' + 'ng-model-options="{ debounce: 10000 }"' + '/>'); var watchSpy = jasmine.createSpy('watchSpy'); $rootScope.$watch(watchSpy); helper.changeInputValueTo('a'); $timeout.flush(2000); expect(watchSpy).not.toHaveBeenCalled(); helper.changeInputValueTo('b'); $timeout.flush(2000); expect(watchSpy).not.toHaveBeenCalled(); helper.changeInputValueTo('c'); $timeout.flush(10000); expect(watchSpy).toHaveBeenCalled(); }); it('should allow selecting different debounce timeouts for each event', function() { var inputElm = helper.compileInput( '<input type="text" ng-model="name" name="alias" ' + 'ng-model-options="{' + 'updateOn: \'default blur mouseup\', ' + 'debounce: { default: 10000, blur: 5000 }' + '}"' + '/>'); helper.changeInputValueTo('a'); expect($rootScope.name).toBeUndefined(); $timeout.flush(6000); expect($rootScope.name).toBeUndefined(); $timeout.flush(4000); expect($rootScope.name).toEqual('a'); helper.changeInputValueTo('b'); browserTrigger(inputElm, 'blur'); $timeout.flush(4000); expect($rootScope.name).toEqual('a'); $timeout.flush(2000); expect($rootScope.name).toEqual('b'); helper.changeInputValueTo('c'); browserTrigger(helper.inputElm, 'mouseup'); // `default` in `debounce` only affects the event triggers that are not defined in updateOn expect($rootScope.name).toEqual('c'); }); it('should use the value of * to debounce all unspecified events', function() { var inputElm = helper.compileInput( '<input type="text" ng-model="name" name="alias" ' + 'ng-model-options="{' + 'updateOn: \'default blur mouseup\', ' + 'debounce: { default: 10000, blur: 5000, \'*\': 15000 }' + '}"' + '/>'); helper.changeInputValueTo('a'); expect($rootScope.name).toBeUndefined(); $timeout.flush(6000); expect($rootScope.name).toBeUndefined(); $timeout.flush(4000); expect($rootScope.name).toEqual('a'); helper.changeInputValueTo('b'); browserTrigger(inputElm, 'blur'); $timeout.flush(4000); expect($rootScope.name).toEqual('a'); $timeout.flush(2000); expect($rootScope.name).toEqual('b'); helper.changeInputValueTo('c'); browserTrigger(helper.inputElm, 'mouseup'); expect($rootScope.name).toEqual('b'); $timeout.flush(10000); // flush default expect($rootScope.name).toEqual('b'); $timeout.flush(5000); expect($rootScope.name).toEqual('c'); }); it('should trigger immediately for the event if not listed in the debounce list', function() { var inputElm = helper.compileInput( '<input type="text" ng-model="name" name="alias" ' + 'ng-model-options="{' + 'updateOn: \'default blur foo\', ' + 'debounce: { blur: 5000 }' + '}"' + '/>'); helper.changeInputValueTo('a'); expect($rootScope.name).toEqual('a'); helper.changeInputValueTo('b'); browserTrigger(inputElm, 'foo'); expect($rootScope.name).toEqual('b'); }); it('should allow selecting different debounce timeouts for each event on checkboxes', function() { var inputElm = helper.compileInput('<input type="checkbox" ng-model="checkbox" ' + 'ng-model-options="{ ' + 'updateOn: \'default blur\', debounce: { default: 10000, blur: 5000 } }"' + '/>'); inputElm[0].checked = false; browserTrigger(inputElm, 'click'); expect($rootScope.checkbox).toBeUndefined(); $timeout.flush(8000); expect($rootScope.checkbox).toBeUndefined(); $timeout.flush(3000); expect($rootScope.checkbox).toBe(true); inputElm[0].checked = true; browserTrigger(inputElm, 'click'); browserTrigger(inputElm, 'blur'); $timeout.flush(3000); expect($rootScope.checkbox).toBe(true); $timeout.flush(3000); expect($rootScope.checkbox).toBe(false); }); it('should allow selecting 0 for non-default debounce timeouts for each event on checkboxes', function() { var inputElm = helper.compileInput('<input type="checkbox" ng-model="checkbox" ' + 'ng-model-options="{ ' + 'updateOn: \'default blur\', debounce: { default: 10000, blur: 0 } }"' + '/>'); inputElm[0].checked = false; browserTrigger(inputElm, 'click'); expect($rootScope.checkbox).toBeUndefined(); $timeout.flush(8000); expect($rootScope.checkbox).toBeUndefined(); $timeout.flush(3000); expect($rootScope.checkbox).toBe(true); inputElm[0].checked = true; browserTrigger(inputElm, 'click'); browserTrigger(inputElm, 'blur'); $timeout.flush(0); expect($rootScope.checkbox).toBe(false); }); it('should flush debounced events when calling $commitViewValue directly', function() { var inputElm = helper.compileInput( '<input type="text" ng-model="name" name="alias" ' + 'ng-model-options="{ debounce: 1000 }" />'); helper.changeInputValueTo('a'); expect($rootScope.name).toEqual(undefined); $rootScope.form.alias.$commitViewValue(); expect($rootScope.name).toEqual('a'); }); it('should cancel debounced events when calling $commitViewValue', function() { var inputElm = helper.compileInput( '<input type="text" ng-model="name" name="alias" ' + 'ng-model-options="{ debounce: 1000 }"/>'); helper.changeInputValueTo('a'); $rootScope.form.alias.$commitViewValue(); expect($rootScope.name).toEqual('a'); $rootScope.form.alias.$setPristine(); $timeout.flush(1000); expect($rootScope.form.alias.$pristine).toBeTruthy(); }); it('should reset input val if rollbackViewValue called during pending update', function() { var inputElm = helper.compileInput( '<input type="text" ng-model="name" name="alias" ' + 'ng-model-options="{ updateOn: \'blur\' }" />'); helper.changeInputValueTo('a'); expect(inputElm.val()).toBe('a'); $rootScope.form.alias.$rollbackViewValue(); expect(inputElm.val()).toBe(''); browserTrigger(inputElm, 'blur'); expect(inputElm.val()).toBe(''); }); it('should allow canceling pending updates', function() { var inputElm = helper.compileInput( '<input type="text" ng-model="name" name="alias" ' + 'ng-model-options="{ updateOn: \'blur\' }" />'); helper.changeInputValueTo('a'); expect($rootScope.name).toEqual(undefined); $rootScope.form.alias.$rollbackViewValue(); expect($rootScope.name).toEqual(undefined); browserTrigger(inputElm, 'blur'); expect($rootScope.name).toEqual(undefined); }); it('should allow canceling debounced updates', function() { var inputElm = helper.compileInput( '<input type="text" ng-model="name" name="alias" ' + 'ng-model-options="{ debounce: 10000 }" />'); helper.changeInputValueTo('a'); expect($rootScope.name).toEqual(undefined); $timeout.flush(2000); $rootScope.form.alias.$rollbackViewValue(); expect($rootScope.name).toEqual(undefined); $timeout.flush(10000); expect($rootScope.name).toEqual(undefined); }); it('should handle model updates correctly even if rollbackViewValue is not invoked', function() { var inputElm = helper.compileInput( '<input type="text" ng-model="name" name="alias" ' + 'ng-model-options="{ updateOn: \'blur\' }" />'); helper.changeInputValueTo('a'); $rootScope.$apply('name = \'b\''); browserTrigger(inputElm, 'blur'); expect($rootScope.name).toBe('b'); }); it('should reset input val if rollbackViewValue called during debounce', function() { var inputElm = helper.compileInput( '<input type="text" ng-model="name" name="alias" ' + 'ng-model-options="{ debounce: 2000 }" />'); helper.changeInputValueTo('a'); expect(inputElm.val()).toBe('a'); $rootScope.form.alias.$rollbackViewValue(); expect(inputElm.val()).toBe(''); $timeout.flush(3000); expect(inputElm.val()).toBe(''); }); }); describe('getterSetter', function() { it('should not try to invoke a model if getterSetter is false', function() { var inputElm = helper.compileInput( '<input type="text" ng-model="name" ' + 'ng-model-options="{ getterSetter: false }" />'); var spy = $rootScope.name = jasmine.createSpy('setterSpy'); helper.changeInputValueTo('a'); expect(spy).not.toHaveBeenCalled(); expect(inputElm.val()).toBe('a'); }); it('should not try to invoke a model if getterSetter is not set', function() { var inputElm = helper.compileInput('<input type="text" ng-model="name" />'); var spy = $rootScope.name = jasmine.createSpy('setterSpy'); helper.changeInputValueTo('a'); expect(spy).not.toHaveBeenCalled(); expect(inputElm.val()).toBe('a'); }); it('should try to invoke a function model if getterSetter is true', function() { var inputElm = helper.compileInput( '<input type="text" ng-model="name" ' + 'ng-model-options="{ getterSetter: true }" />'); var spy = $rootScope.name = jasmine.createSpy('setterSpy').and.callFake(function() { return 'b'; }); $rootScope.$apply(); expect(inputElm.val()).toBe('b'); helper.changeInputValueTo('a'); expect(inputElm.val()).toBe('b'); expect(spy).toHaveBeenCalledWith('a'); expect($rootScope.name).toBe(spy); }); it('should assign to non-function models if getterSetter is true', function() { var inputElm = helper.compileInput( '<input type="text" ng-model="name" ' + 'ng-model-options="{ getterSetter: true }" />'); $rootScope.name = 'c'; helper.changeInputValueTo('d'); expect(inputElm.val()).toBe('d'); expect($rootScope.name).toBe('d'); }); it('should fail on non-assignable model binding if getterSetter is false', function() { expect(function() { var inputElm = helper.compileInput('<input type="text" ng-model="accessor(user, \'name\')" />'); }).toThrowMinErr('ngModel', 'nonassign', 'Expression \'accessor(user, \'name\')\' is non-assignable.'); }); it('should not fail on non-assignable model binding if getterSetter is true', function() { var inputElm = helper.compileInput( '<input type="text" ng-model="accessor(user, \'name\')" ' + 'ng-model-options="{ getterSetter: true }" />'); }); it('should invoke a model in the correct context if getterSetter is true', function() { var inputElm = helper.compileInput( '<input type="text" ng-model="someService.getterSetter" ' + 'ng-model-options="{ getterSetter: true }" />'); $rootScope.someService = { value: 'a', getterSetter: function(newValue) { this.value = newValue || this.value; return this.value; } }; spyOn($rootScope.someService, 'getterSetter').and.callThrough(); $rootScope.$apply(); expect(inputElm.val()).toBe('a'); expect($rootScope.someService.getterSetter).toHaveBeenCalledWith(); expect($rootScope.someService.value).toBe('a'); helper.changeInputValueTo('b'); expect($rootScope.someService.getterSetter).toHaveBeenCalledWith('b'); expect($rootScope.someService.value).toBe('b'); $rootScope.someService.value = 'c'; $rootScope.$apply(); expect(inputElm.val()).toBe('c'); expect($rootScope.someService.getterSetter).toHaveBeenCalledWith(); }); }); describe('allowInvalid', function() { it('should assign invalid values to the scope if allowInvalid is true', function() { var inputElm = helper.compileInput('<input type="text" name="input" ng-model="value" maxlength="1" ' + 'ng-model-options="{allowInvalid: true}" />'); helper.changeInputValueTo('12345'); expect($rootScope.value).toBe('12345'); expect(inputElm).toBeInvalid(); }); it('should not assign not parsable values to the scope if allowInvalid is true', function() { var inputElm = helper.compileInput('<input type="number" name="input" ng-model="value" ' + 'ng-model-options="{allowInvalid: true}" />', { valid: false, badInput: true }); helper.changeInputValueTo('abcd'); expect($rootScope.value).toBeUndefined(); expect(inputElm).toBeInvalid(); }); it('should update the scope before async validators execute if allowInvalid is true', function() { var inputElm = helper.compileInput('<input type="text" name="input" ng-model="value" ' + 'ng-model-options="{allowInvalid: true}" />'); var defer; $rootScope.form.input.$asyncValidators.promiseValidator = function(value) { defer = $q.defer(); return defer.promise; }; helper.changeInputValueTo('12345'); expect($rootScope.value).toBe('12345'); expect($rootScope.form.input.$pending.promiseValidator).toBe(true); defer.reject(); $rootScope.$digest(); expect($rootScope.value).toBe('12345'); expect(inputElm).toBeInvalid(); }); it('should update the view before async validators execute if allowInvalid is true', function() { var inputElm = helper.compileInput('<input type="text" name="input" ng-model="value" ' + 'ng-model-options="{allowInvalid: true}" />'); var defer; $rootScope.form.input.$asyncValidators.promiseValidator = function(value) { defer = $q.defer(); return defer.promise; }; $rootScope.$apply('value = \'12345\''); expect(inputElm.val()).toBe('12345'); expect($rootScope.form.input.$pending.promiseValidator).toBe(true); defer.reject(); $rootScope.$digest(); expect(inputElm.val()).toBe('12345'); expect(inputElm).toBeInvalid(); }); it('should not call ng-change listeners twice if the model did not change with allowInvalid', function() { var inputElm = helper.compileInput('<input type="text" name="input" ng-model="value" ' + 'ng-model-options="{allowInvalid: true}" ng-change="changed()" />'); $rootScope.changed = jasmine.createSpy('changed'); $rootScope.form.input.$parsers.push(function(value) { return 'modelValue'; }); helper.changeInputValueTo('input1'); expect($rootScope.value).toBe('modelValue'); expect($rootScope.changed).toHaveBeenCalledOnce(); helper.changeInputValueTo('input2'); expect($rootScope.value).toBe('modelValue'); expect($rootScope.changed).toHaveBeenCalledOnce(); }); }); }); describe('on directives with `replace: true`', function() { var $rootScope, $compile; beforeEach(module(function($compileProvider) { $compileProvider.directive('foo', valueFn({ replace: true, template: '<input type="text" ng-model-options="{debounce: 1000}" />' })); })); beforeEach(inject(function(_$compile_, _$rootScope_) { $compile = _$compile_; $rootScope = _$rootScope_; })); it('should get initialized in time for `ngModel` on the original element', function() { var inputElm = $compile('<foo ng-model="value"></foo>')($rootScope); var ngModelCtrl = inputElm.controller('ngModel'); expect(ngModelCtrl.$options.getOption('debounce')).toBe(1000); }); }); }); });