diff --git a/angularFiles.js b/angularFiles.js index d0b0d8dd8504..696bd8e78ec8 100755 --- a/angularFiles.js +++ b/angularFiles.js @@ -65,6 +65,7 @@ var angularFiles = { 'src/ng/directive/ngList.js', 'src/ng/directive/ngModel.js', 'src/ng/directive/ngNonBindable.js', + 'src/ng/directive/ngOptions.js', 'src/ng/directive/ngPluralize.js', 'src/ng/directive/ngRepeat.js', 'src/ng/directive/ngShowHide.js', diff --git a/src/ng/directive/ngOptions.js b/src/ng/directive/ngOptions.js new file mode 100644 index 000000000000..f6ec5f227c0b --- /dev/null +++ b/src/ng/directive/ngOptions.js @@ -0,0 +1,612 @@ +'use strict'; + +/* global jqLiteRemove */ + +var ngOptionsMinErr = minErr('ngOptions'); + +/** + * @ngdoc directive + * @name ngOptions + * @restrict A + * + * @description + * + * The `ngOptions` attribute can be used to dynamically generate a list of `'); + + scope.$apply(function() { + scope.blankVal = 'so blank'; + scope.values = [{name: 'A'}]; + }); + + // check blank option is first and is compiled + expect(element.find('option').length).toBe(2); + option = element.find('option').eq(0); + expect(option.val()).toBe(''); + expect(option.text()).toBe('blank is so blank'); + + scope.$apply(function() { + scope.blankVal = 'not so blank'; + }); + + // check blank option is first and is compiled + expect(element.find('option').length).toBe(2); + option = element.find('option').eq(0); + expect(option.val()).toBe(''); + expect(option.text()).toBe('blank is not so blank'); + }); + + + it('should support binding via ngBindTemplate directive', function() { + var option; + createSingleSelect(''); + + scope.$apply(function() { + scope.blankVal = 'so blank'; + scope.values = [{name: 'A'}]; + }); + + // check blank option is first and is compiled + expect(element.find('option').length).toBe(2); + option = element.find('option').eq(0); + expect(option.val()).toBe(''); + expect(option.text()).toBe('blank is so blank'); + }); + + + it('should support biding via ngBind attribute', function() { + var option; + createSingleSelect(''); + + scope.$apply(function() { + scope.blankVal = 'is blank'; + scope.values = [{name: 'A'}]; + }); + + // check blank option is first and is compiled + expect(element.find('option').length).toBe(2); + option = element.find('option').eq(0); + expect(option.val()).toBe(''); + expect(option.text()).toBe('is blank'); + }); + + + it('should be rendered with the attributes preserved', function() { + var option; + createSingleSelect(''); + + scope.$apply(function() { + scope.blankVal = 'is blank'; + }); + + // check blank option is first and is compiled + option = element.find('option').eq(0); + expect(option.hasClass('coyote')).toBeTruthy(); + expect(option.attr('id')).toBe('road-runner'); + expect(option.attr('custom-attr')).toBe('custom-attr'); + }); + + it('should be selected, if it is available and no other option is selected', function() { + // selectedIndex is used here because jqLite incorrectly reports element.val() + scope.$apply(function() { + scope.values = [{name: 'A'}]; + }); + createSingleSelect(true); + // ensure the first option (the blank option) is selected + expect(element[0].selectedIndex).toEqual(0); + scope.$digest(); + // ensure the option has not changed following the digest + expect(element[0].selectedIndex).toEqual(0); + }); + }); + + + describe('on change', function() { + + it('should update model on change', function() { + createSingleSelect(); + + scope.$apply(function() { + scope.values = [{name: 'A'}, {name: 'B'}]; + scope.selected = scope.values[0]; + }); + + expect(element).toEqualSelectValue(scope.selected); + + setSelectValue(element, 1); + expect(scope.selected).toEqual(scope.values[1]); + }); + + + it('should update model on change through expression', function() { + createSelect({ + 'ng-model': 'selected', + 'ng-options': 'item.id as item.name for item in values' + }); + + scope.$apply(function() { + scope.values = [{id: 10, name: 'A'}, {id: 20, name: 'B'}]; + scope.selected = scope.values[0].id; + }); + + expect(element).toEqualSelectValue(scope.selected); + + setSelectValue(element, 1); + expect(scope.selected).toEqual(scope.values[1].id); + }); + + + it('should update model to null on change', function() { + createSingleSelect(true); + + scope.$apply(function() { + scope.values = [{name: 'A'}, {name: 'B'}]; + scope.selected = scope.values[0]; + }); + + element.val(''); + browserTrigger(element, 'change'); + expect(scope.selected).toEqual(null); + }); + + + // Regression https://github.com/angular/angular.js/issues/7855 + it('should update the model with ng-change', function() { + createSelect({ + 'ng-change':'change()', + 'ng-model':'selected', + 'ng-options':'value for value in values' + }); + + scope.$apply(function() { + scope.values = ['A', 'B']; + scope.selected = 'A'; + }); + + scope.change = function() { + scope.selected = 'A'; + }; + + element.find('option')[1].selected = true; + + browserTrigger(element, 'change'); + expect(element.find('option')[0].selected).toBeTruthy(); + expect(scope.selected).toEqual('A'); + }); + }); + + describe('disabled blank', function() { + it('should select disabled blank by default', function() { + var html = ''; + scope.$apply(function() { + scope.choices = ['A', 'B', 'C']; + }); + + compile(html); + + var options = element.find('option'); + var optionToSelect = options.eq(0); + expect(optionToSelect.text()).toBe('Choose One'); + expect(optionToSelect.prop('selected')).toBe(true); + expect(element[0].value).toBe(''); + + dealoc(element); + }); + + + it('should select disabled blank by default when select is required', function() { + var html = ''; + scope.$apply(function() { + scope.choices = ['A', 'B', 'C']; + }); + + compile(html); + + var options = element.find('option'); + var optionToSelect = options.eq(0); + expect(optionToSelect.text()).toBe('Choose One'); + expect(optionToSelect.prop('selected')).toBe(true); + expect(element[0].value).toBe(''); + + dealoc(element); + }); + }); + + describe('select-many', function() { + + it('should read multiple selection', function() { + createMultiSelect(); + + scope.$apply(function() { + scope.values = [{name: 'A'}, {name: 'B'}]; + scope.selected = []; + }); + + expect(element.find('option').length).toEqual(2); + expect(element.find('option')[0].selected).toBeFalsy(); + expect(element.find('option')[1].selected).toBeFalsy(); + + scope.$apply(function() { + scope.selected.push(scope.values[1]); + }); + + expect(element.find('option').length).toEqual(2); + expect(element.find('option')[0].selected).toBeFalsy(); + expect(element.find('option')[1].selected).toBeTruthy(); + + scope.$apply(function() { + scope.selected.push(scope.values[0]); + }); + + expect(element.find('option').length).toEqual(2); + expect(element.find('option')[0].selected).toBeTruthy(); + expect(element.find('option')[1].selected).toBeTruthy(); + }); + + + it('should update model on change', function() { + createMultiSelect(); + + scope.$apply(function() { + scope.values = [{name: 'A'}, {name: 'B'}]; + scope.selected = []; + }); + + element.find('option')[0].selected = true; + + browserTrigger(element, 'change'); + expect(scope.selected).toEqual([scope.values[0]]); + }); + + + it('should select from object', function() { + createSelect({ + 'ng-model':'selected', + 'multiple':true, + 'ng-options':'key as value for (key,value) in values' + }); + scope.values = {'0':'A', '1':'B'}; + + scope.selected = ['1']; + scope.$digest(); + expect(element.find('option')[1].selected).toBe(true); + + element.find('option')[0].selected = true; + browserTrigger(element, 'change'); + expect(scope.selected).toEqual(['0', '1']); + + element.find('option')[1].selected = false; + browserTrigger(element, 'change'); + expect(scope.selected).toEqual(['0']); + }); + + it('should deselect all options when model is emptied', function() { + createMultiSelect(); + scope.$apply(function() { + scope.values = [{name: 'A'}, {name: 'B'}]; + scope.selected = [scope.values[0]]; + }); + expect(element.find('option')[0].selected).toEqual(true); + + scope.$apply(function() { + scope.selected.pop(); + }); + + expect(element.find('option')[0].selected).toEqual(false); + }); + }); + + + describe('ngRequired', function() { + + it('should allow bindings on ngRequired', function() { + createSelect({ + 'ng-model': 'value', + 'ng-options': 'item.name for item in values', + 'ng-required': 'required' + }, true); + + + scope.$apply(function() { + scope.values = [{name: 'A', id: 1}, {name: 'B', id: 2}]; + scope.required = false; + }); + + element.val(''); + browserTrigger(element, 'change'); + expect(element).toBeValid(); + + scope.$apply(function() { + scope.required = true; + }); + expect(element).toBeInvalid(); + + scope.$apply(function() { + scope.value = scope.values[0]; + }); + expect(element).toBeValid(); + + element.val(''); + browserTrigger(element, 'change'); + expect(element).toBeInvalid(); + + scope.$apply(function() { + scope.required = false; + }); + expect(element).toBeValid(); + }); + + + it('should treat an empty array as invalid when `multiple` attribute used', function() { + createSelect({ + 'ng-model': 'value', + 'ng-options': 'item.name for item in values', + 'ng-required': 'required', + 'multiple': '' + }, true); + + scope.$apply(function() { + scope.value = []; + scope.values = [{name: 'A', id: 1}, {name: 'B', id: 2}]; + scope.required = true; + }); + expect(element).toBeInvalid(); + + scope.$apply(function() { + // ngModelWatch does not set objectEquality flag + // array must be replaced in order to trigger $formatters + scope.value = [scope.values[0]]; + }); + expect(element).toBeValid(); + }); + + + it('should allow falsy values as values', function() { + createSelect({ + 'ng-model': 'value', + 'ng-options': 'item.value as item.name for item in values', + 'ng-required': 'required' + }, true); + + scope.$apply(function() { + scope.values = [{name: 'True', value: true}, {name: 'False', value: false}]; + scope.required = false; + }); + + setSelectValue(element, 2); + expect(element).toBeValid(); + expect(scope.value).toBe(false); + + scope.$apply('required = true'); + expect(element).toBeValid(); + expect(scope.value).toBe(false); + }); + }); + + describe('ngModelCtrl', function() { + it('should prefix the model value with the word "the" using $parsers', function() { + createSelect({ + 'name': 'select', + 'ng-model': 'value', + 'ng-options': 'item for item in [\'first\', \'second\', \'third\', \'fourth\']' + }); + + scope.form.select.$parsers.push(function(value) { + return 'the ' + value; + }); + + setSelectValue(element, 3); + expect(scope.value).toBe('the third'); + expect(element).toEqualSelectValue('third'); + }); + + it('should prefix the view value with the word "the" using $formatters', function() { + createSelect({ + 'name': 'select', + 'ng-model': 'value', + 'ng-options': 'item for item in [\'the first\', \'the second\', \'the third\', \'the fourth\']' + }); + + scope.form.select.$formatters.push(function(value) { + return 'the ' + value; + }); + + scope.$apply(function() { + scope.value = 'third'; + }); + expect(element).toEqualSelectValue('the third'); + }); + + it('should fail validation when $validators fail', function() { + createSelect({ + 'name': 'select', + 'ng-model': 'value', + 'ng-options': 'item for item in [\'first\', \'second\', \'third\', \'fourth\']' + }); + + scope.form.select.$validators.fail = function() { + return false; + }; + + setSelectValue(element, 3); + expect(element).toBeInvalid(); + expect(scope.value).toBeUndefined(); + expect(element).toEqualSelectValue('third'); + }); + + it('should pass validation when $validators pass', function() { + createSelect({ + 'name': 'select', + 'ng-model': 'value', + 'ng-options': 'item for item in [\'first\', \'second\', \'third\', \'fourth\']' + }); + + scope.form.select.$validators.pass = function() { + return true; + }; + + setSelectValue(element, 3); + expect(element).toBeValid(); + expect(scope.value).toBe('third'); + expect(element).toEqualSelectValue('third'); + }); + + it('should fail validation when $asyncValidators fail', inject(function($q, $rootScope) { + var defer; + createSelect({ + 'name': 'select', + 'ng-model': 'value', + 'ng-options': 'item for item in [\'first\', \'second\', \'third\', \'fourth\']' + }); + + scope.form.select.$asyncValidators.async = function() { + defer = $q.defer(); + return defer.promise; + }; + + setSelectValue(element, 3); + expect(scope.form.select.$pending).toBeDefined(); + expect(scope.value).toBeUndefined(); + expect(element).toEqualSelectValue('third'); + + defer.reject(); + $rootScope.$digest(); + expect(scope.form.select.$pending).toBeUndefined(); + expect(scope.value).toBeUndefined(); + expect(element).toEqualSelectValue('third'); + })); + + it('should pass validation when $asyncValidators pass', inject(function($q, $rootScope) { + var defer; + createSelect({ + 'name': 'select', + 'ng-model': 'value', + 'ng-options': 'item for item in [\'first\', \'second\', \'third\', \'fourth\']' + }); + + scope.form.select.$asyncValidators.async = function() { + defer = $q.defer(); + return defer.promise; + }; + + setSelectValue(element, 3); + expect(scope.form.select.$pending).toBeDefined(); + expect(scope.value).toBeUndefined(); + expect(element).toEqualSelectValue('third'); + + defer.resolve(); + $rootScope.$digest(); + expect(scope.form.select.$pending).toBeUndefined(); + expect(scope.value).toBe('third'); + expect(element).toEqualSelectValue('third'); + })); + }); +}); diff --git a/test/ng/directive/selectSpec.js b/test/ng/directive/selectSpec.js index 2a624130fc69..725c87ddec97 100644 --- a/test/ng/directive/selectSpec.js +++ b/test/ng/directive/selectSpec.js @@ -10,6 +10,10 @@ describe('select', function() { scope.$apply(); } + function unknownValue(value) { + return '? ' + hashKey(value) + ' ?'; + } + beforeEach(inject(function($rootScope, _$compile_) { scope = $rootScope.$new(); //create a child scope because the root scope can't be $destroy-ed $compile = _$compile_; @@ -47,7 +51,8 @@ describe('select', function() { forEach(this.actual.find('option'), function(option) { optionGroup = option.parentNode.label || ''; actualValues[optionGroup] = actualValues[optionGroup] || []; - actualValues[optionGroup].push(option.label); + // IE9 doesn't populate the label property from the text property like other browsers + actualValues[optionGroup].push(option.label || option.text); }); this.message = function() { @@ -55,25 +60,6 @@ describe('select', function() { }; return equals(expected, actualValues); - }, - - toEqualOption: function(value, text, label) { - var errors = []; - if (this.actual.attr('value') !== value) { - errors.push('Expected option value "' + this.actual.attr('value') + '" to equal "' + value + '"'); - } - if (text && this.actual.text() !== text) { - errors.push('Expected option value "' + this.actual.attr('value') + '" to equal "' + value + '"'); - } - if (label && this.actual.attr('label') !== label) { - errors.push('Expected option value "' + this.actual.attr('value') + '" to equal "' + value + '"'); - } - - this.message = function() { - return errors.join('\n'); - }; - - return errors.length === 0; } }); @@ -250,31 +236,6 @@ describe('select', function() { expect(scope.robot).toBe(''); }); - it('should not be set when an option is selected and options are set asynchronously', - inject(function($timeout) { - compile(''); - - scope.$apply(function() { - scope.model = 0; - }); - - $timeout(function() { - scope.options = [ - {id: 0, label: 'x'}, - {id: 1, label: 'y'} - ]; - }, 0); - - $timeout.flush(); - - var options = element.find('option'); - - expect(options.length).toEqual(2); - expect(options.eq(0)).toEqualOption('0', 'x'); - expect(options.eq(1)).toEqualOption('1', 'y'); - }) - ); describe('interactions with repeated options', function() { @@ -333,7 +294,7 @@ describe('select', function() { '' + ''); - expect(element).toEqualSelect(['? undefined:undefined ?'], 'c3p0', 'r2d2'); + expect(element).toEqualSelect([unknownValue(undefined)], 'c3p0', 'r2d2'); scope.$apply(function() { scope.robot = 'r2d2'; @@ -344,7 +305,7 @@ describe('select', function() { scope.$apply(function() { scope.robot = "wallee"; }); - expect(element).toEqualSelect(['? string:wallee ?'], 'c3p0', 'r2d2'); + expect(element).toEqualSelect([unknownValue('wallee')], 'c3p0', 'r2d2'); }); @@ -362,7 +323,7 @@ describe('select', function() { scope.$apply(function() { scope.robot = null; }); - expect(element).toEqualSelect(['? object:null ?'], '', 'c3p0', 'r2d2'); + expect(element).toEqualSelect([unknownValue(null)], '', 'c3p0', 'r2d2'); scope.$apply(function() { scope.robot = 'r2d2'; @@ -385,7 +346,7 @@ describe('select', function() { '' + ''); - expect(element).toEqualSelect(['? string:wallee ?'], '', 'c3p0', 'r2d2'); + expect(element).toEqualSelect([unknownValue('wallee')], '', 'c3p0', 'r2d2'); scope.$apply(function() { scope.robot = 'r2d2'; @@ -400,13 +361,13 @@ describe('select', function() { compile(''); - expect(element).toEqualSelect(['? undefined:undefined ?']); + expect(element).toEqualSelect([unknownValue(undefined)]); expect(scope.robot).toBeUndefined(); scope.$apply(function() { scope.robot = 'r2d2'; }); - expect(element).toEqualSelect(['? string:r2d2 ?']); + expect(element).toEqualSelect([unknownValue('r2d2')]); expect(scope.robot).toBe('r2d2'); scope.$apply(function() { @@ -428,7 +389,7 @@ describe('select', function() { scope.$apply(function() { scope.robot = 'r2d2'; }); - expect(element).toEqualSelect(['? string:r2d2 ?'], ''); + expect(element).toEqualSelect([unknownValue('r2d2')], ''); expect(scope.robot).toBe('r2d2'); scope.$apply(function() { @@ -452,7 +413,7 @@ describe('select', function() { scope.$apply(function() { scope.robots.pop(); }); - expect(element).toEqualSelect(['? string:r2d2 ?'], 'c3p0'); + expect(element).toEqualSelect([unknownValue('r2d2')], 'c3p0'); expect(scope.robot).toBe('r2d2'); scope.$apply(function() { @@ -464,1941 +425,405 @@ describe('select', function() { scope.$apply(function() { delete scope.robots; }); - expect(element).toEqualSelect(['? string:r2d2 ?']); + expect(element).toEqualSelect([unknownValue('r2d2')]); expect(scope.robot).toBe('r2d2'); }); - - describe('selectController.hasOption', function() { - it('should return false for options shifted via ngOptions', function() { - scope.robots = [ - {value: 1, label: 'c3p0'}, - {value: 2, label: 'r2d2'} - ]; - - compile(''); - - var selectCtrl = element.data().$selectController; - - scope.$apply(function() { - scope.robots.shift(); - }); - - expect(selectCtrl.hasOption('c3p0')).toBe(false); - expect(selectCtrl.hasOption('r2d2')).toBe(true); - }); - - - it('should return false for options popped via ngOptions', function() { - scope.robots = [ - {value: 1, label: 'c3p0'}, - {value: 2, label: 'r2d2'} - ]; - - compile(''); - - var selectCtrl = element.data().$selectController; - - scope.$apply(function() { - scope.robots.pop(); - }); - - expect(selectCtrl.hasOption('c3p0')).toBe(true); - expect(selectCtrl.hasOption('r2d2')).toBe(false); - }); - - - it('should return true for options added via ngOptions', function() { - scope.robots = [ - {value: 2, label: 'r2d2'} - ]; - - compile(''); - - var selectCtrl = element.data().$selectController; - - scope.$apply(function() { - scope.robots.unshift({value: 1, label: 'c3p0'}); - }); - - expect(selectCtrl.hasOption('c3p0')).toBe(true); - expect(selectCtrl.hasOption('r2d2')).toBe(true); - }); - - - it('should keep all the options when changing the model', function() { - compile(''); - var selectCtrl = element.controller('select'); - scope.$apply(function() { - scope.mySelect = 'C'; - }); - expect(selectCtrl.hasOption('A')).toBe(true); - expect(selectCtrl.hasOption('B')).toBe(true); - expect(selectCtrl.hasOption('C')).toBe(true); - expect(element).toEqualSelectWithOptions({'': ['A', 'B', 'C']}); - }); - - - it('should be able to detect when elements move from a previous group', function() { - scope.values = [ - {name: 'A'}, - {name: 'B', group: 'first'}, - {name: 'C', group: 'first'}, - {name: 'D', group: 'first'}, - {name: 'E', group: 'second'} - ]; - - compile(''); - var selectCtrl = element.data().$selectController; - - scope.$apply(function() { - scope.values[3] = {name: 'D', group: 'second'}; - scope.values.shift(); - }); - expect(selectCtrl.hasOption('A')).toBe(false); - expect(selectCtrl.hasOption('B')).toBe(true); - expect(selectCtrl.hasOption('C')).toBe(true); - expect(selectCtrl.hasOption('D')).toBe(true); - expect(selectCtrl.hasOption('E')).toBe(true); - expect(element).toEqualSelectWithOptions({'': [''], 'first':['B', 'C'], 'second': ['D', 'E']}); - }); - - - it('should be able to detect when elements move from a following group', function() { - scope.values = [ - {name: 'A'}, - {name: 'B', group: 'first'}, - {name: 'C', group: 'first'}, - {name: 'D', group: 'second'}, - {name: 'E', group: 'second'} - ]; - - compile(''); - var selectCtrl = element.data().$selectController; - - scope.$apply(function() { - scope.values[3].group = 'first'; - scope.values.shift(); - }); - expect(selectCtrl.hasOption('A')).toBe(false); - expect(selectCtrl.hasOption('B')).toBe(true); - expect(selectCtrl.hasOption('C')).toBe(true); - expect(selectCtrl.hasOption('D')).toBe(true); - expect(selectCtrl.hasOption('E')).toBe(true); - expect(element).toEqualSelectWithOptions({'': [''], 'first':['B', 'C', 'D'], 'second': ['E']}); - }); - - - it('should be able to detect when an element is replaced with an element from a previous group', function() { - scope.values = [ - {name: 'A'}, - {name: 'B', group: 'first'}, - {name: 'C', group: 'first'}, - {name: 'D', group: 'first'}, - {name: 'E', group: 'second'}, - {name: 'F', group: 'second'} - ]; - - compile(''); - var selectCtrl = element.data().$selectController; - - scope.$apply(function() { - scope.values[3].group = 'second'; - scope.values.pop(); - }); - expect(selectCtrl.hasOption('A')).toBe(true); - expect(selectCtrl.hasOption('B')).toBe(true); - expect(selectCtrl.hasOption('C')).toBe(true); - expect(selectCtrl.hasOption('D')).toBe(true); - expect(selectCtrl.hasOption('E')).toBe(true); - expect(selectCtrl.hasOption('F')).toBe(false); - expect(element).toEqualSelectWithOptions({'': ['', 'A'], 'first':['B', 'C'], 'second': ['D', 'E']}); - }); - - - it('should be able to detect when element is replaced with an element from a following group', function() { - scope.values = [ - {name: 'A'}, - {name: 'B', group: 'first'}, - {name: 'C', group: 'first'}, - {name: 'D', group: 'second'}, - {name: 'E', group: 'second'} - ]; - - compile(''); - var selectCtrl = element.data().$selectController; - - scope.$apply(function() { - scope.values[3].group = 'first'; - scope.values.splice(2, 1); - }); - expect(selectCtrl.hasOption('A')).toBe(true); - expect(selectCtrl.hasOption('B')).toBe(true); - expect(selectCtrl.hasOption('C')).toBe(false); - expect(selectCtrl.hasOption('D')).toBe(true); - expect(selectCtrl.hasOption('E')).toBe(true); - expect(element).toEqualSelectWithOptions({'': ['', 'A'], 'first':['B', 'D'], 'second': ['E']}); - }); - - - it('should be able to detect when an element is removed', function() { - scope.values = [ - {name: 'A'}, - {name: 'B', group: 'first'}, - {name: 'C', group: 'first'}, - {name: 'D', group: 'second'}, - {name: 'E', group: 'second'} - ]; - - compile(''); - var selectCtrl = element.data().$selectController; - - scope.$apply(function() { - scope.values.splice(3, 1); - }); - expect(selectCtrl.hasOption('A')).toBe(true); - expect(selectCtrl.hasOption('B')).toBe(true); - expect(selectCtrl.hasOption('C')).toBe(true); - expect(selectCtrl.hasOption('D')).toBe(false); - expect(selectCtrl.hasOption('E')).toBe(true); - expect(element).toEqualSelectWithOptions({'': ['', 'A'], 'first':['B', 'C'], 'second': ['E']}); - }); - - - it('should be able to detect when a group is removed', function() { - scope.values = [ - {name: 'A'}, - {name: 'B', group: 'first'}, - {name: 'C', group: 'first'}, - {name: 'D', group: 'second'}, - {name: 'E', group: 'second'} - ]; - - compile(''); - var selectCtrl = element.data().$selectController; - - scope.$apply(function() { - scope.values.splice(3, 2); - }); - expect(selectCtrl.hasOption('A')).toBe(true); - expect(selectCtrl.hasOption('B')).toBe(true); - expect(selectCtrl.hasOption('C')).toBe(true); - expect(selectCtrl.hasOption('D')).toBe(false); - expect(selectCtrl.hasOption('E')).toBe(false); - expect(element).toEqualSelectWithOptions({'': ['', 'A'], 'first':['B', 'C']}); - }); - }); - }); - }); - }); - - - describe('select-multiple', function() { - - it('should support type="select-multiple"', function() { - compile( - ''); - - scope.$apply(function() { - scope.selection = ['A']; - }); - - expect(element).toEqualSelect(['A'], 'B'); - - scope.$apply(function() { - scope.selection.push('B'); }); - expect(element).toEqualSelect(['A'], ['B']); }); - it('should work with optgroups', function() { - compile(''); + }); - expect(element).toEqualSelect('A', 'B'); - expect(scope.selection).toBeUndefined(); - scope.$apply(function() { - scope.selection = ['A']; - }); - expect(element).toEqualSelect(['A'], 'B'); + describe('selectController.hasOption', function() { - scope.$apply(function() { - scope.selection.push('B'); - }); - expect(element).toEqualSelect(['A'], ['B']); - }); + function compileRepeatedOptions() { + compile(''); + } - it('should require', function() { + function compileGroupedOptions() { compile( - '' + + '' + + '' + + '' + + '' + ''); + } - scope.$apply(function() { - scope.selection = []; - }); - - expect(scope.form.select.$error.required).toBeTruthy(); - expect(element).toBeInvalid(); - expect(element).toBePristine(); - - scope.$apply(function() { - scope.selection = ['A']; - }); - - expect(element).toBeValid(); - expect(element).toBePristine(); - - element[0].value = 'B'; - browserTrigger(element, 'change'); - expect(element).toBeValid(); - expect(element).toBeDirty(); - }); - - describe('selectController.hasOption', function() { - it('should return false for options shifted via ngOptions', function() { + describe('flat options', function() { + it('should return false for options shifted via ngRepeat', function() { scope.robots = [ {value: 1, label: 'c3p0'}, {value: 2, label: 'r2d2'} ]; - compile(''); + compileRepeatedOptions(); - var selectCtrl = element.data().$selectController; + var selectCtrl = element.controller('select'); scope.$apply(function() { scope.robots.shift(); }); - expect(selectCtrl.hasOption('c3p0')).toBe(false); - expect(selectCtrl.hasOption('r2d2')).toBe(true); + expect(selectCtrl.hasOption('1')).toBe(false); + expect(selectCtrl.hasOption('2')).toBe(true); }); - it('should return false for options popped via ngOptions', function() { + + it('should return false for options popped via ngRepeat', function() { scope.robots = [ {value: 1, label: 'c3p0'}, {value: 2, label: 'r2d2'} ]; - compile(''); + compileRepeatedOptions(); - var selectCtrl = element.data().$selectController; + var selectCtrl = element.controller('select'); scope.$apply(function() { scope.robots.pop(); }); - expect(selectCtrl.hasOption('c3p0')).toBe(true); - expect(selectCtrl.hasOption('r2d2')).toBe(false); + expect(selectCtrl.hasOption('1')).toBe(true); + expect(selectCtrl.hasOption('2')).toBe(false); }); - it('should return true for options added via ngOptions', function() { + + it('should return true for options added via ngRepeat', function() { scope.robots = [ {value: 2, label: 'r2d2'} ]; - compile(''); + compileRepeatedOptions(); - var selectCtrl = element.data().$selectController; + var selectCtrl = element.controller('select'); scope.$apply(function() { scope.robots.unshift({value: 1, label: 'c3p0'}); }); - expect(selectCtrl.hasOption('c3p0')).toBe(true); - expect(selectCtrl.hasOption('r2d2')).toBe(true); - }); - }); - }); - - - describe('ngOptions', function() { - function createSelect(attrs, blank, unknown) { - var html = 'blank') : '') + - (unknown ? (isString(unknown) ? unknown : '') : '') + - ''; - - compile(html); - } - function createSingleSelect(blank, unknown) { - createSelect({ - 'ng-model':'selected', - 'ng-options':'value.name for value in values' - }, blank, unknown); - } - function createMultiSelect(blank, unknown) { - createSelect({ - 'ng-model':'selected', - 'multiple':true, - 'ng-options':'value.name for value in values' - }, blank, unknown); - } + it('should keep all the options when changing the model', function() { - describe('selectAs expression', function() { - beforeEach(function() { - scope.arr = [{id: 10, label: 'ten'}, {id:20, label: 'twenty'}]; - scope.obj = {'10': {score: 10, label: 'ten'}, '20': {score: 20, label: 'twenty'}}; - }); + compile(''); - it('should support single select with array source', function() { - createSelect({ - 'ng-model': 'selected', - 'ng-options': 'item.id as item.label for item in arr' - }); + var selectCtrl = element.controller('select'); scope.$apply(function() { - scope.selected = 10; + scope.mySelect = 'C'; }); - expect(element.val()).toBe('0'); - element.val('1'); - browserTrigger(element, 'change'); - expect(scope.selected).toBe(20); + expect(selectCtrl.hasOption('A')).toBe(true); + expect(selectCtrl.hasOption('B')).toBe(true); + expect(selectCtrl.hasOption('C')).toBe(true); + expect(element).toEqualSelectWithOptions({'': ['A', 'B', 'C']}); }); + }); - it('should support multi select with array source', function() { - createSelect({ - 'ng-model': 'selected', - 'multiple': true, - 'ng-options': 'item.id as item.label for item in arr' - }); - - scope.$apply(function() { - scope.selected = [10,20]; - }); - expect(element.val()).toEqual(['0','1']); - expect(scope.selected).toEqual([10,20]); + describe('grouped options', function() { - element.children()[0].selected = false; - browserTrigger(element, 'change'); - expect(scope.selected).toEqual([20]); - expect(element.val()).toEqual(['1']); - }); + it('should be able to detect when elements move from a previous group', function() { + scope.values = [{name: 'A'}]; + scope.groups = [ + { + name: 'first', + values: [ + {name: 'B'}, + {name: 'C'}, + {name: 'D'} + ] + }, + { + name: 'second', + values: [ + {name: 'E'} + ] + } + ]; + compileGroupedOptions(); - it('should support single select with object source', function() { - createSelect({ - 'ng-model': 'selected', - 'ng-options': 'val.score as val.label for (key, val) in obj' - }); + var selectCtrl = element.controller('select'); scope.$apply(function() { - scope.selected = 10; + var itemD = scope.groups[0].values.pop(); + scope.groups[1].values.unshift(itemD); + scope.values.shift(); }); - expect(element.val()).toBe('10'); - element.val('20'); - browserTrigger(element, 'change'); - expect(scope.selected).toBe(20); + expect(selectCtrl.hasOption('A')).toBe(false); + expect(selectCtrl.hasOption('B')).toBe(true); + expect(selectCtrl.hasOption('C')).toBe(true); + expect(selectCtrl.hasOption('D')).toBe(true); + expect(selectCtrl.hasOption('E')).toBe(true); + expect(element).toEqualSelectWithOptions({'': [''], 'first':['B', 'C'], 'second': ['D', 'E']}); }); - it('should support multi select with object source', function() { - createSelect({ - 'ng-model': 'selected', - 'multiple': true, - 'ng-options': 'val.score as val.label for (key, val) in obj' - }); + it('should be able to detect when elements move from a following group', function() { + scope.values = [{name: 'A'}]; + scope.groups = [ + { + name: 'first', + values: [ + {name: 'B'}, + {name: 'C'} + ] + }, + { + name: 'second', + values: [ + {name: 'D'}, + {name: 'E'} + ] + } + ]; + + compileGroupedOptions(); + + var selectCtrl = element.controller('select'); scope.$apply(function() { - scope.selected = [10,20]; + var itemD = scope.groups[1].values.shift(); + scope.groups[0].values.push(itemD); + scope.values.shift(); }); - expect(element.val()).toEqual(['10','20']); - - element.children()[0].selected = false; - browserTrigger(element, 'change'); - expect(scope.selected).toEqual([20]); - expect(element.val()).toEqual(['20']); + expect(selectCtrl.hasOption('A')).toBe(false); + expect(selectCtrl.hasOption('B')).toBe(true); + expect(selectCtrl.hasOption('C')).toBe(true); + expect(selectCtrl.hasOption('D')).toBe(true); + expect(selectCtrl.hasOption('E')).toBe(true); + expect(element).toEqualSelectWithOptions({'': [''], 'first':['B', 'C', 'D'], 'second': ['E']}); }); - }); - describe('trackBy expression', function() { - beforeEach(function() { - scope.arr = [{id: 10, label: 'ten'}, {id:20, label: 'twenty'}]; - scope.obj = {'1': {score: 10, label: 'ten'}, '2': {score: 20, label: 'twenty'}}; - }); - + it('should be able to detect when an element is replaced with an element from a previous group', function() { + scope.values = [{name: 'A'}]; + scope.groups = [ + { + name: 'first', + values: [ + {name: 'B'}, + {name: 'C'}, + {name: 'D'} + ] + }, + { + name: 'second', + values: [ + {name: 'E'}, + {name: 'F'} + ] + } + ]; - it('should set the result of track by expression to element value', function() { - createSelect({ - 'ng-model': 'selected', - 'ng-options': 'item.label for item in arr track by item.id' - }); + compileGroupedOptions(); - scope.$apply(function() { - scope.selected = scope.arr[0]; - }); - expect(element.val()).toBe('10'); + var selectCtrl = element.controller('select'); scope.$apply(function() { - scope.arr[0] = {id: 10, label: 'new ten'}; + var itemD = scope.groups[0].values.pop(); + scope.groups[1].values.unshift(itemD); + scope.groups[1].values.pop(); }); - expect(element.val()).toBe('10'); - - element.children()[1].selected = 'selected'; - browserTrigger(element, 'change'); - expect(scope.selected).toEqual(scope.arr[1]); + expect(selectCtrl.hasOption('A')).toBe(true); + expect(selectCtrl.hasOption('B')).toBe(true); + expect(selectCtrl.hasOption('C')).toBe(true); + expect(selectCtrl.hasOption('D')).toBe(true); + expect(selectCtrl.hasOption('E')).toBe(true); + expect(selectCtrl.hasOption('F')).toBe(false); + expect(element).toEqualSelectWithOptions({'': ['', 'A'], 'first':['B', 'C'], 'second': ['D', 'E']}); }); - it('should use the tracked expression as option value', function() { - createSelect({ - 'ng-model': 'selected', - 'ng-options': 'item.label for item in arr track by item.id' - }); - - var options = element.find('option'); - expect(options.length).toEqual(3); - expect(options.eq(0)).toEqualOption('?', ''); - expect(options.eq(1)).toEqualOption('10', 'ten'); - expect(options.eq(2)).toEqualOption('20', 'twenty'); - }); + it('should be able to detect when element is replaced with an element from a following group', function() { + scope.values = [{name: 'A'}]; + scope.groups = [ + { + name: 'first', + values: [ + {name: 'B'}, + {name: 'C'} + ] + }, + { + name: 'second', + values: [ + {name: 'D'}, + {name: 'E'} + ] + } + ]; - it('should preserve value even when reference has changed (single&array)', function() { - createSelect({ - 'ng-model': 'selected', - 'ng-options': 'item.label for item in arr track by item.id' - }); + compileGroupedOptions(); - scope.$apply(function() { - scope.selected = scope.arr[0]; - }); - expect(element.val()).toBe('10'); + var selectCtrl = element.controller('select'); scope.$apply(function() { - scope.arr[0] = {id: 10, label: 'new ten'}; + scope.groups[0].values.pop(); + var itemD = scope.groups[1].values.shift(); + scope.groups[0].values.push(itemD); }); - expect(element.val()).toBe('10'); - - element.children()[1].selected = 1; - browserTrigger(element, 'change'); - expect(scope.selected).toEqual(scope.arr[1]); + expect(selectCtrl.hasOption('A')).toBe(true); + expect(selectCtrl.hasOption('B')).toBe(true); + expect(selectCtrl.hasOption('C')).toBe(false); + expect(selectCtrl.hasOption('D')).toBe(true); + expect(selectCtrl.hasOption('E')).toBe(true); + expect(element).toEqualSelectWithOptions({'': ['', 'A'], 'first':['B', 'D'], 'second': ['E']}); }); - it('should preserve value even when reference has changed (multi&array)', function() { - createSelect({ - 'ng-model': 'selected', - 'multiple': true, - 'ng-options': 'item.label for item in arr track by item.id' - }); + it('should be able to detect when an element is removed', function() { + scope.values = [{name: 'A'}]; + scope.groups = [ + { + name: 'first', + values: [ + {name: 'B'}, + {name: 'C'} + ] + }, + { + name: 'second', + values: [ + {name: 'D'}, + {name: 'E'} + ] + } + ]; - scope.$apply(function() { - scope.selected = scope.arr; - }); - expect(element.val()).toEqual(['10','20']); + compileGroupedOptions(); + + var selectCtrl = element.controller('select'); scope.$apply(function() { - scope.arr[0] = {id: 10, label: 'new ten'}; + scope.groups[1].values.shift(); }); - expect(element.val()).toEqual(['10','20']); - - element.children()[0].selected = false; - browserTrigger(element, 'change'); - expect(scope.selected).toEqual([scope.arr[1]]); + expect(selectCtrl.hasOption('A')).toBe(true); + expect(selectCtrl.hasOption('B')).toBe(true); + expect(selectCtrl.hasOption('C')).toBe(true); + expect(selectCtrl.hasOption('D')).toBe(false); + expect(selectCtrl.hasOption('E')).toBe(true); + expect(element).toEqualSelectWithOptions({'': ['', 'A'], 'first':['B', 'C'], 'second': ['E']}); }); - it('should preserve value even when reference has changed (single&object)', function() { - createSelect({ - 'ng-model': 'selected', - 'ng-options': 'val.label for (key, val) in obj track by val.score' - }); + it('should be able to detect when a group is removed', function() { + scope.values = [{name: 'A'}]; + scope.groups = [ + { + name: 'first', + values: [ + {name: 'B'}, + {name: 'C'} + ] + }, + { + name: 'second', + values: [ + {name: 'D'}, + {name: 'E'} + ] + } + ]; - scope.$apply(function() { - scope.selected = scope.obj['1']; - }); - expect(element.val()).toBe('10'); + compileGroupedOptions(); + + var selectCtrl = element.controller('select'); scope.$apply(function() { - scope.obj['1'] = {score: 10, label: 'ten'}; + scope.groups.pop(); }); - expect(element.val()).toBe('10'); - - element.val('20'); - browserTrigger(element, 'change'); - expect(scope.selected).toBe(scope.obj['2']); + expect(selectCtrl.hasOption('A')).toBe(true); + expect(selectCtrl.hasOption('B')).toBe(true); + expect(selectCtrl.hasOption('C')).toBe(true); + expect(selectCtrl.hasOption('D')).toBe(false); + expect(selectCtrl.hasOption('E')).toBe(false); + expect(element).toEqualSelectWithOptions({'': ['', 'A'], 'first':['B', 'C']}); }); + }); + }); + describe('select-multiple', function() { - it('should preserve value even when reference has changed (multi&object)', function() { - createSelect({ - 'ng-model': 'selected', - 'multiple': true, - 'ng-options': 'val.label for (key, val) in obj track by val.score' - }); - - scope.$apply(function() { - scope.selected = [scope.obj['1']]; - }); - expect(element.val()).toEqual(['10']); - - scope.$apply(function() { - scope.obj['1'] = {score: 10, label: 'ten'}; - }); - expect(element.val()).toEqual(['10']); + it('should support type="select-multiple"', function() { + compile( + ''); - element.children()[1].selected = 'selected'; - browserTrigger(element, 'change'); - expect(scope.selected).toEqual([scope.obj['1'], scope.obj['2']]); + scope.$apply(function() { + scope.selection = ['A']; }); - }); + expect(element).toEqualSelect(['A'], 'B'); - /** - * This behavior is broken and should probably be cleaned up later as track by and select as - * aren't compatible. - */ - describe('selectAs+trackBy expression', function() { - beforeEach(function() { - scope.arr = [{subItem: {label: 'ten', id: 10}}, {subItem: {label: 'twenty', id: 20}}]; - scope.obj = {'10': {subItem: {id: 10, label: 'ten'}}, '20': {subItem: {id: 20, label: 'twenty'}}}; + scope.$apply(function() { + scope.selection.push('B'); }); + expect(element).toEqualSelect(['A'], ['B']); + }); - it('It should use the "value" variable to represent items in the array as well as for the ' + - 'selected values in track by expression (single&array)', function() { - createSelect({ - 'ng-model': 'selected', - 'ng-options': 'item.subItem as item.subItem.label for item in arr track by (item.id || item.subItem.id)' - }); - - // First test model -> view + it('should work with optgroups', function() { + compile(''); - scope.$apply(function() { - scope.selected = scope.arr[0].subItem; - }); - expect(element.val()).toEqual('10'); + expect(element).toEqualSelect('A', 'B'); + expect(scope.selection).toBeUndefined(); - scope.$apply(function() { - scope.selected = scope.arr[1].subItem; - }); - expect(element.val()).toEqual('20'); + scope.$apply(function() { + scope.selection = ['A']; + }); + expect(element).toEqualSelect(['A'], 'B'); - // Now test view -> model + scope.$apply(function() { + scope.selection.push('B'); + }); + expect(element).toEqualSelect(['A'], ['B']); + }); - element.val('10'); - browserTrigger(element, 'change'); - expect(scope.selected).toBe(scope.arr[0].subItem); + it('should require', function() { + compile( + ''); - // Now reload the array - scope.$apply(function() { - scope.arr = [{ - subItem: {label: 'new ten', id: 10} - },{ - subItem: {label: 'new twenty', id: 20} - }]; - }); - expect(element.val()).toBe('10'); - expect(scope.selected.id).toBe(10); + scope.$apply(function() { + scope.selection = []; }); + expect(scope.form.select.$error.required).toBeTruthy(); + expect(element).toBeInvalid(); + expect(element).toBePristine(); - it('It should use the "value" variable to represent items in the array as well as for the ' + - 'selected values in track by expression (multiple&array)', function() { - createSelect({ - 'ng-model': 'selected', - 'multiple': true, - 'ng-options': 'item.subItem as item.subItem.label for item in arr track by (item.id || item.subItem.id)' - }); + scope.$apply(function() { + scope.selection = ['A']; + }); - // First test model -> view - - scope.$apply(function() { - scope.selected = [scope.arr[0].subItem]; - }); - expect(element.val()).toEqual(['10']); - - scope.$apply(function() { - scope.selected = [scope.arr[1].subItem]; - }); - expect(element.val()).toEqual(['20']); - - // Now test view -> model - - element.find('option')[0].selected = true; - element.find('option')[1].selected = false; - browserTrigger(element, 'change'); - expect(scope.selected).toEqual([scope.arr[0].subItem]); - - // Now reload the array - scope.$apply(function() { - scope.arr = [{ - subItem: {label: 'new ten', id: 10} - },{ - subItem: {label: 'new twenty', id: 20} - }]; - }); - expect(element.val()).toEqual(['10']); - expect(scope.selected[0].id).toEqual(10); - expect(scope.selected.length).toBe(1); - }); - - - it('It should use the "value" variable to represent items in the array as well as for the ' + - 'selected values in track by expression (multiple&object)', function() { - createSelect({ - 'ng-model': 'selected', - 'multiple': true, - 'ng-options': 'val.subItem as val.subItem.label for (key, val) in obj track by (val.id || val.subItem.id)' - }); - - // First test model -> view - - scope.$apply(function() { - scope.selected = [scope.obj['10'].subItem]; - }); - expect(element.val()).toEqual(['10']); - - - scope.$apply(function() { - scope.selected = [scope.obj['10'].subItem]; - }); - expect(element.val()).toEqual(['10']); - - // Now test view -> model - - element.find('option')[0].selected = true; - element.find('option')[1].selected = false; - browserTrigger(element, 'change'); - expect(scope.selected).toEqual([scope.obj['10'].subItem]); - - // Now reload the object - scope.$apply(function() { - scope.obj = { - '10': { - subItem: {label: 'new ten', id: 10} - }, - '20': { - subItem: {label: 'new twenty', id: 20} - } - }; - }); - expect(element.val()).toEqual(['10']); - expect(scope.selected[0].id).toBe(10); - expect(scope.selected.length).toBe(1); - }); - - - it('It should use the "value" variable to represent items in the array as well as for the ' + - 'selected values in track by expression (single&object)', function() { - createSelect({ - 'ng-model': 'selected', - 'ng-options': 'val.subItem as val.subItem.label for (key, val) in obj track by (val.id || val.subItem.id)' - }); - - // First test model -> view - - scope.$apply(function() { - scope.selected = scope.obj['10'].subItem; - }); - expect(element.val()).toEqual('10'); - - - scope.$apply(function() { - scope.selected = scope.obj['10'].subItem; - }); - expect(element.val()).toEqual('10'); - - // Now test view -> model - - element.find('option')[0].selected = true; - browserTrigger(element, 'change'); - expect(scope.selected).toEqual(scope.obj['10'].subItem); - - // Now reload the object - scope.$apply(function() { - scope.obj = { - '10': { - subItem: {label: 'new ten', id: 10} - }, - '20': { - subItem: {label: 'new twenty', id: 20} - } - }; - }); - expect(element.val()).toEqual('10'); - expect(scope.selected.id).toBe(10); - }); - }); - - - it('should throw when not formated "? for ? in ?"', function() { - expect(function() { - compile(''); - }).toThrowMinErr('ngOptions', 'iexp', /Expected expression in form of/); - }); - - - it('should render a list', function() { - createSingleSelect(); - - scope.$apply(function() { - scope.values = [{name: 'A'}, {name: 'B'}, {name: 'C'}]; - scope.selected = scope.values[0]; - }); - - var options = element.find('option'); - expect(options.length).toEqual(3); - expect(options.eq(0)).toEqualOption('0', 'A'); - expect(options.eq(1)).toEqualOption('1', 'B'); - expect(options.eq(2)).toEqualOption('2', 'C'); - }); - - it('should render zero as a valid display value', function() { - createSingleSelect(); - - scope.$apply(function() { - scope.values = [{name: 0}, {name: 1}, {name: 2}]; - scope.selected = scope.values[0]; - }); - - var options = element.find('option'); - expect(options.length).toEqual(3); - expect(options.eq(0)).toEqualOption('0', '0'); - expect(options.eq(1)).toEqualOption('1', '1'); - expect(options.eq(2)).toEqualOption('2', '2'); - }); - - - it('should render an object', function() { - createSelect({ - 'ng-model': 'selected', - 'ng-options': 'value as key for (key, value) in object' - }); - - scope.$apply(function() { - scope.object = {'red': 'FF0000', 'green': '00FF00', 'blue': '0000FF'}; - scope.selected = scope.object.red; - }); - - var options = element.find('option'); - expect(options.length).toEqual(3); - expect(options.eq(0)).toEqualOption('blue', 'blue'); - expect(options.eq(1)).toEqualOption('green', 'green'); - expect(options.eq(2)).toEqualOption('red', 'red'); - expect(options[2].selected).toEqual(true); - - scope.$apply(function() { - scope.object.azur = '8888FF'; - }); - - options = element.find('option'); - expect(options[3].selected).toEqual(true); - }); - - - it('should grow list', function() { - createSingleSelect(); - - scope.$apply(function() { - scope.values = []; - }); - - expect(element.find('option').length).toEqual(1); // because we add special empty option - expect(element.find('option')).toEqualOption('?',''); - - scope.$apply(function() { - scope.values.push({name:'A'}); - scope.selected = scope.values[0]; - }); - - expect(element.find('option').length).toEqual(1); - expect(element.find('option')).toEqualOption('0', 'A'); - - scope.$apply(function() { - scope.values.push({name:'B'}); - }); - - expect(element.find('option').length).toEqual(2); - expect(element.find('option').eq(0)).toEqualOption('0', 'A'); - expect(element.find('option').eq(1)).toEqualOption('1', 'B'); - }); - - - it('should shrink list', function() { - createSingleSelect(); - - scope.$apply(function() { - scope.values = [{name:'A'}, {name:'B'}, {name:'C'}]; - scope.selected = scope.values[0]; - }); - - expect(element.find('option').length).toEqual(3); - - scope.$apply(function() { - scope.values.pop(); - }); - - expect(element.find('option').length).toEqual(2); - expect(element.find('option').eq(0)).toEqualOption('0', 'A'); - expect(element.find('option').eq(1)).toEqualOption('1', 'B'); - - scope.$apply(function() { - scope.values.pop(); - }); - - expect(element.find('option').length).toEqual(1); - expect(element.find('option')).toEqualOption('0', 'A'); - - scope.$apply(function() { - scope.values.pop(); - scope.selected = null; - }); - - expect(element.find('option').length).toEqual(1); // we add back the special empty option - }); - - - it('should shrink and then grow list', function() { - createSingleSelect(); - - scope.$apply(function() { - scope.values = [{name:'A'}, {name:'B'}, {name:'C'}]; - scope.selected = scope.values[0]; - }); - - expect(element.find('option').length).toEqual(3); - - scope.$apply(function() { - scope.values = [{name: '1'}, {name: '2'}]; - scope.selected = scope.values[0]; - }); - - expect(element.find('option').length).toEqual(2); - - scope.$apply(function() { - scope.values = [{name: 'A'}, {name: 'B'}, {name: 'C'}]; - scope.selected = scope.values[0]; - }); - - expect(element.find('option').length).toEqual(3); - }); - - - it('should update list', function() { - createSingleSelect(); - - scope.$apply(function() { - scope.values = [{name: 'A'}, {name: 'B'}, {name: 'C'}]; - scope.selected = scope.values[0]; - }); - - scope.$apply(function() { - scope.values = [{name: 'B'}, {name: 'C'}, {name: 'D'}]; - scope.selected = scope.values[0]; - }); - - var options = element.find('option'); - expect(options.length).toEqual(3); - expect(options.eq(0)).toEqualOption('0', 'B'); - expect(options.eq(1)).toEqualOption('1', 'C'); - expect(options.eq(2)).toEqualOption('2', 'D'); - }); - - - it('should preserve existing options', function() { - createSingleSelect(true); - - scope.$apply(function() { - scope.values = []; - }); - - expect(element.find('option').length).toEqual(1); - - scope.$apply(function() { - scope.values = [{name: 'A'}]; - scope.selected = scope.values[0]; - }); - - expect(element.find('option').length).toEqual(2); - expect(jqLite(element.find('option')[0]).text()).toEqual('blank'); - expect(jqLite(element.find('option')[1]).text()).toEqual('A'); - - scope.$apply(function() { - scope.values = []; - scope.selected = null; - }); - - expect(element.find('option').length).toEqual(1); - expect(jqLite(element.find('option')[0]).text()).toEqual('blank'); - }); - - it('should ignore $ and $$ properties', function() { - createSelect({ - 'ng-options': 'key as value for (key, value) in object', - 'ng-model': 'selected' - }); - - scope.$apply(function() { - scope.object = {'regularProperty': 'visible', '$$private': 'invisible', '$property': 'invisible'}; - scope.selected = 'regularProperty'; - }); - - var options = element.find('option'); - expect(options.length).toEqual(1); - expect(options.eq(0)).toEqualOption('regularProperty', 'visible'); - }); - - it('should allow expressions over multiple lines', function() { - scope.isNotFoo = function(item) { - return item.name !== 'Foo'; - }; - - createSelect({ - 'ng-options': 'key.id\n' + - 'for key in object\n' + - '| filter:isNotFoo', - 'ng-model': 'selected' - }); - - scope.$apply(function() { - scope.object = [{'id': 1, 'name': 'Foo'}, - {'id': 2, 'name': 'Bar'}, - {'id': 3, 'name': 'Baz'}]; - scope.selected = scope.object[0]; - }); - - var options = element.find('option'); - expect(options.length).toEqual(3); - expect(options.eq(1)).toEqualOption('0', '2'); - expect(options.eq(2)).toEqualOption('1', '3'); - }); - - it('should not update selected property of an option element on digest with no change event', - function() { - // ng-options="value.name for value in values" - // ng-model="selected" - createSingleSelect(); - - scope.$apply(function() { - scope.values = [{name: 'A'}, {name: 'B'}, {name: 'C'}]; - scope.selected = scope.values[0]; - }); - - var options = element.find('option'); - - expect(scope.selected).toEqual({ name: 'A' }); - expect(options.eq(0).prop('selected')).toBe(true); - expect(options.eq(1).prop('selected')).toBe(false); - - var optionToSelect = options.eq(1); - - expect(optionToSelect.text()).toBe('B'); - - optionToSelect.prop('selected', true); - scope.$digest(); - - expect(optionToSelect.prop('selected')).toBe(true); - expect(scope.selected).toBe(scope.values[0]); - }); - - // bug fix #9621 - it('should update the label property', function() { - // ng-options="value.name for value in values" - // ng-model="selected" - createSingleSelect(); - - scope.$apply(function() { - scope.values = [{name: 'A'}, {name: 'B'}, {name: 'C'}]; - scope.selected = scope.values[0]; - }); - - var options = element.find('option'); - expect(options.eq(0).prop('label')).toEqual('A'); - expect(options.eq(1).prop('label')).toEqual('B'); - expect(options.eq(2).prop('label')).toEqual('C'); - }); - - describe('binding', function() { - - it('should bind to scope value', function() { - createSingleSelect(); - - scope.$apply(function() { - scope.values = [{name: 'A'}, {name: 'B'}]; - scope.selected = scope.values[0]; - }); - - expect(element.val()).toEqual('0'); - - scope.$apply(function() { - scope.selected = scope.values[1]; - }); - - expect(element.val()).toEqual('1'); - }); - - - it('should bind to scope value and group', function() { - createSelect({ - 'ng-model': 'selected', - 'ng-options': 'item.name group by item.group for item in values' - }); - - scope.$apply(function() { - scope.values = [{name: 'A'}, - {name: 'B', group: 'first'}, - {name: 'C', group: 'second'}, - {name: 'D', group: 'first'}, - {name: 'E', group: 'second'}]; - scope.selected = scope.values[3]; - }); - - expect(element.val()).toEqual('3'); - - var first = jqLite(element.find('optgroup')[0]); - var b = jqLite(first.find('option')[0]); - var d = jqLite(first.find('option')[1]); - expect(first.attr('label')).toEqual('first'); - expect(b.text()).toEqual('B'); - expect(d.text()).toEqual('D'); - - var second = jqLite(element.find('optgroup')[1]); - var c = jqLite(second.find('option')[0]); - var e = jqLite(second.find('option')[1]); - expect(second.attr('label')).toEqual('second'); - expect(c.text()).toEqual('C'); - expect(e.text()).toEqual('E'); - - scope.$apply(function() { - scope.selected = scope.values[0]; - }); - - expect(element.val()).toEqual('0'); - }); - - - it('should bind to scope value and track/identify objects', function() { - createSelect({ - 'ng-model': 'selected', - 'ng-options': 'item.name for item in values track by item.id' - }); - - scope.$apply(function() { - scope.values = [{id: 1, name: 'first'}, - {id: 2, name: 'second'}, - {id: 3, name: 'third'}, - {id: 4, name: 'forth'}]; - scope.selected = scope.values[1]; - }); - - expect(element.val()).toEqual('2'); - - var first = jqLite(element.find('option')[0]); - expect(first.text()).toEqual('first'); - expect(first.attr('value')).toEqual('1'); - var forth = jqLite(element.find('option')[3]); - expect(forth.text()).toEqual('forth'); - expect(forth.attr('value')).toEqual('4'); - - scope.$apply(function() { - scope.selected = scope.values[3]; - }); - - expect(element.val()).toEqual('4'); - }); - - - it('should bind to scope value through experession', function() { - createSelect({ - 'ng-model': 'selected', - 'ng-options': 'item.id as item.name for item in values' - }); - - scope.$apply(function() { - scope.values = [{id: 10, name: 'A'}, {id: 20, name: 'B'}]; - scope.selected = scope.values[0].id; - }); - - expect(element.val()).toEqual('0'); - - scope.$apply(function() { - scope.selected = scope.values[1].id; - }); - - expect(element.val()).toEqual('1'); - }); - - it('should update options in the DOM', function() { - compile( - '' - ); - - scope.$apply(function() { - scope.values = [{id: 10, name: 'A'}, {id: 20, name: 'B'}]; - scope.selected = scope.values[0].id; - }); - - scope.$apply(function() { - scope.values[0].name = 'C'; - }); - - var options = element.find('option'); - expect(options.length).toEqual(2); - expect(options.eq(0)).toEqualOption('0', 'C'); - expect(options.eq(1)).toEqualOption('1', 'B'); - }); - - - it('should update options in the DOM from object source', function() { - compile( - '' - ); - - scope.$apply(function() { - scope.values = {a: {id: 10, name: 'A'}, b: {id: 20, name: 'B'}}; - scope.selected = scope.values.a.id; - }); - - scope.$apply(function() { - scope.values.a.name = 'C'; - }); - - var options = element.find('option'); - expect(options.length).toEqual(2); - expect(options.eq(0)).toEqualOption('a', 'C'); - expect(options.eq(1)).toEqualOption('b', 'B'); - }); - - - it('should bind to object key', function() { - createSelect({ - 'ng-model': 'selected', - 'ng-options': 'key as value for (key, value) in object' - }); - - scope.$apply(function() { - scope.object = {red: 'FF0000', green: '00FF00', blue: '0000FF'}; - scope.selected = 'green'; - }); - - expect(element.val()).toEqual('green'); - - scope.$apply(function() { - scope.selected = 'blue'; - }); - - expect(element.val()).toEqual('blue'); - }); - - - it('should bind to object value', function() { - createSelect({ - 'ng-model': 'selected', - 'ng-options': 'value as key for (key, value) in object' - }); - - scope.$apply(function() { - scope.object = {red: 'FF0000', green: '00FF00', blue:'0000FF'}; - scope.selected = '00FF00'; - }); - - expect(element.val()).toEqual('green'); - - scope.$apply(function() { - scope.selected = '0000FF'; - }); - - expect(element.val()).toEqual('blue'); - }); - - - it('should insert a blank option if bound to null', function() { - createSingleSelect(); - - scope.$apply(function() { - scope.values = [{name: 'A'}]; - scope.selected = null; - }); - - expect(element.find('option').length).toEqual(2); - expect(element.val()).toEqual(''); - expect(jqLite(element.find('option')[0]).val()).toEqual(''); - - scope.$apply(function() { - scope.selected = scope.values[0]; - }); - - expect(element.val()).toEqual('0'); - expect(element.find('option').length).toEqual(1); - }); - - - it('should reuse blank option if bound to null', function() { - createSingleSelect(true); - - scope.$apply(function() { - scope.values = [{name: 'A'}]; - scope.selected = null; - }); - - expect(element.find('option').length).toEqual(2); - expect(element.val()).toEqual(''); - expect(jqLite(element.find('option')[0]).val()).toEqual(''); - - scope.$apply(function() { - scope.selected = scope.values[0]; - }); - - expect(element.val()).toEqual('0'); - expect(element.find('option').length).toEqual(2); - }); - - - it('should insert a unknown option if bound to something not in the list', function() { - createSingleSelect(); - - scope.$apply(function() { - scope.values = [{name: 'A'}]; - scope.selected = {}; - }); - - expect(element.find('option').length).toEqual(2); - expect(element.val()).toEqual('?'); - expect(jqLite(element.find('option')[0]).val()).toEqual('?'); - - scope.$apply(function() { - scope.selected = scope.values[0]; - }); - - expect(element.val()).toEqual('0'); - expect(element.find('option').length).toEqual(1); - }); - - - it('should select correct input if previously selected option was "?"', function() { - createSingleSelect(); - - scope.$apply(function() { - scope.values = [{name: 'A'}, {name: 'B'}]; - scope.selected = {}; - }); - - expect(element.find('option').length).toEqual(3); - expect(element.val()).toEqual('?'); - expect(element.find('option').eq(0).val()).toEqual('?'); - - browserTrigger(element.find('option').eq(1)); - expect(element.val()).toEqual('0'); - expect(element.find('option').eq(0).prop('selected')).toBeTruthy(); - expect(element.find('option').length).toEqual(2); - }); - - - it('should use exact same values as values in scope with one-time bindings', function() { - scope.values = [{name: 'A'}, {name: 'B'}]; - scope.selected = scope.values[0]; - createSelect({ - 'ng-model': 'selected', - 'ng-options': 'value.name for value in ::values' - }); - - browserTrigger(element.find('option').eq(1)); - - expect(scope.selected).toBe(scope.values[1]); - }); - - - it('should ensure that at least one option element has the "selected" attribute', function() { - createSelect({ - 'ng-model': 'selected', - 'ng-options': 'item.id as item.name for item in values' - }); - - scope.$apply(function() { - scope.values = [{id: 10, name: 'A'}, {id: 20, name: 'B'}]; - }); - expect(element.val()).toEqual('?'); - expect(element.find('option').eq(0).attr('selected')).toEqual('selected'); - - scope.$apply(function() { - scope.selected = 10; - }); - // Here the ? option should disappear and the first real option should have selected attribute - expect(element.val()).toEqual('0'); - expect(element.find('option').eq(0).attr('selected')).toEqual('selected'); - - // Here the selected value is changed but we don't change the selected attribute - scope.$apply(function() { - scope.selected = 20; - }); - expect(element.val()).toEqual('1'); - expect(element.find('option').eq(0).attr('selected')).toEqual('selected'); - - scope.$apply(function() { - scope.values.push({id: 30, name: 'C'}); - }); - expect(element.val()).toEqual('1'); - expect(element.find('option').eq(0).attr('selected')).toEqual('selected'); - - // Here the ? option should reappear and have selected attribute - scope.$apply(function() { - scope.selected = undefined; - }); - expect(element.val()).toEqual('?'); - expect(element.find('option').eq(0).attr('selected')).toEqual('selected'); - }); - - - it('should select the correct option for selectAs and falsy values', function() { - scope.values = [{value: 0, label: 'zero'}, {value: 1, label: 'one'}]; - scope.selected = ''; - createSelect({ - 'ng-model': 'selected', - 'ng-options': 'option.value as option.label for option in values' - }); - - var option = element.find('option').eq(0); - expect(option.val()).toBe('?'); - expect(option.text()).toBe(''); - }); - }); - - - describe('blank option', function() { - - it('should be compiled as template, be watched and updated', function() { - var option; - createSingleSelect(''); - - scope.$apply(function() { - scope.blankVal = 'so blank'; - scope.values = [{name: 'A'}]; - }); - - // check blank option is first and is compiled - expect(element.find('option').length).toBe(2); - option = element.find('option').eq(0); - expect(option.val()).toBe(''); - expect(option.text()).toBe('blank is so blank'); - - scope.$apply(function() { - scope.blankVal = 'not so blank'; - }); - - // check blank option is first and is compiled - expect(element.find('option').length).toBe(2); - option = element.find('option').eq(0); - expect(option.val()).toBe(''); - expect(option.text()).toBe('blank is not so blank'); - }); - - - it('should support binding via ngBindTemplate directive', function() { - var option; - createSingleSelect(''); - - scope.$apply(function() { - scope.blankVal = 'so blank'; - scope.values = [{name: 'A'}]; - }); - - // check blank option is first and is compiled - expect(element.find('option').length).toBe(2); - option = element.find('option').eq(0); - expect(option.val()).toBe(''); - expect(option.text()).toBe('blank is so blank'); - }); - - - it('should support biding via ngBind attribute', function() { - var option; - createSingleSelect(''); - - scope.$apply(function() { - scope.blankVal = 'is blank'; - scope.values = [{name: 'A'}]; - }); - - // check blank option is first and is compiled - expect(element.find('option').length).toBe(2); - option = element.find('option').eq(0); - expect(option.val()).toBe(''); - expect(option.text()).toBe('is blank'); - }); - - - it('should be rendered with the attributes preserved', function() { - var option; - createSingleSelect(''); - - scope.$apply(function() { - scope.blankVal = 'is blank'; - }); - - // check blank option is first and is compiled - option = element.find('option').eq(0); - expect(option.hasClass('coyote')).toBeTruthy(); - expect(option.attr('id')).toBe('road-runner'); - expect(option.attr('custom-attr')).toBe('custom-attr'); - }); - - it('should be selected, if it is available and no other option is selected', function() { - // selectedIndex is used here because jqLite incorrectly reports element.val() - scope.$apply(function() { - scope.values = [{name: 'A'}]; - }); - createSingleSelect(true); - // ensure the first option (the blank option) is selected - expect(element[0].selectedIndex).toEqual(0); - scope.$digest(); - // ensure the option has not changed following the digest - expect(element[0].selectedIndex).toEqual(0); - }); - }); - - - describe('on change', function() { - - it('should update model on change', function() { - createSingleSelect(); - - scope.$apply(function() { - scope.values = [{name: 'A'}, {name: 'B'}]; - scope.selected = scope.values[0]; - }); - - expect(element.val()).toEqual('0'); - - element.val('1'); - browserTrigger(element, 'change'); - expect(scope.selected).toEqual(scope.values[1]); - }); - - - it('should update model on change through expression', function() { - createSelect({ - 'ng-model': 'selected', - 'ng-options': 'item.id as item.name for item in values' - }); - - scope.$apply(function() { - scope.values = [{id: 10, name: 'A'}, {id: 20, name: 'B'}]; - scope.selected = scope.values[0].id; - }); - - expect(element.val()).toEqual('0'); - - element.val('1'); - browserTrigger(element, 'change'); - expect(scope.selected).toEqual(scope.values[1].id); - }); - - - it('should update model to null on change', function() { - createSingleSelect(true); - - scope.$apply(function() { - scope.values = [{name: 'A'}, {name: 'B'}]; - scope.selected = scope.values[0]; - element.val('0'); - }); - - element.val(''); - browserTrigger(element, 'change'); - expect(scope.selected).toEqual(null); - }); - - - // Regression https://github.com/angular/angular.js/issues/7855 - it('should update the model with ng-change', function() { - createSelect({ - 'ng-change':'change()', - 'ng-model':'selected', - 'ng-options':'value for value in values' - }); - - scope.$apply(function() { - scope.values = ['A', 'B']; - scope.selected = 'A'; - }); - - scope.change = function() { - scope.selected = 'A'; - }; - - element.find('option')[1].selected = true; - - browserTrigger(element, 'change'); - expect(element.find('option')[0].selected).toBeTruthy(); - expect(scope.selected).toEqual('A'); - }); - }); - - describe('disabled blank', function() { - it('should select disabled blank by default', function() { - var html = ''; - scope.$apply(function() { - scope.choices = ['A', 'B', 'C']; - }); - - compile(html); - - var options = element.find('option'); - var optionToSelect = options.eq(0); - expect(optionToSelect.text()).toBe('Choose One'); - expect(optionToSelect.prop('selected')).toBe(true); - expect(element[0].value).toBe(''); - - dealoc(element); - }); - - - it('should select disabled blank by default when select is required', function() { - var html = ''; - scope.$apply(function() { - scope.choices = ['A', 'B', 'C']; - }); - - compile(html); - - var options = element.find('option'); - var optionToSelect = options.eq(0); - expect(optionToSelect.text()).toBe('Choose One'); - expect(optionToSelect.prop('selected')).toBe(true); - expect(element[0].value).toBe(''); - - dealoc(element); - }); - }); - - describe('select-many', function() { - - it('should read multiple selection', function() { - createMultiSelect(); - - scope.$apply(function() { - scope.values = [{name: 'A'}, {name: 'B'}]; - scope.selected = []; - }); - - expect(element.find('option').length).toEqual(2); - expect(element.find('option')[0].selected).toBeFalsy(); - expect(element.find('option')[1].selected).toBeFalsy(); - - scope.$apply(function() { - scope.selected.push(scope.values[1]); - }); - - expect(element.find('option').length).toEqual(2); - expect(element.find('option')[0].selected).toBeFalsy(); - expect(element.find('option')[1].selected).toBeTruthy(); - - scope.$apply(function() { - scope.selected.push(scope.values[0]); - }); - - expect(element.find('option').length).toEqual(2); - expect(element.find('option')[0].selected).toBeTruthy(); - expect(element.find('option')[1].selected).toBeTruthy(); - }); - - - it('should update model on change', function() { - createMultiSelect(); - - scope.$apply(function() { - scope.values = [{name: 'A'}, {name: 'B'}]; - scope.selected = []; - }); - - element.find('option')[0].selected = true; - - browserTrigger(element, 'change'); - expect(scope.selected).toEqual([scope.values[0]]); - }); - - - it('should select from object', function() { - createSelect({ - 'ng-model':'selected', - 'multiple':true, - 'ng-options':'key as value for (key,value) in values' - }); - scope.values = {'0':'A', '1':'B'}; - - scope.selected = ['1']; - scope.$digest(); - expect(element.find('option')[1].selected).toBe(true); - - element.find('option')[0].selected = true; - browserTrigger(element, 'change'); - expect(scope.selected).toEqual(['0', '1']); - - element.find('option')[1].selected = false; - browserTrigger(element, 'change'); - expect(scope.selected).toEqual(['0']); - }); - - it('should deselect all options when model is emptied', function() { - createMultiSelect(); - scope.$apply(function() { - scope.values = [{name: 'A'}, {name: 'B'}]; - scope.selected = [scope.values[0]]; - }); - expect(element.find('option')[0].selected).toEqual(true); - - scope.$apply(function() { - scope.selected.pop(); - }); - - expect(element.find('option')[0].selected).toEqual(false); - }); - }); - - - describe('ngRequired', function() { - - it('should allow bindings on ngRequired', function() { - createSelect({ - 'ng-model': 'value', - 'ng-options': 'item.name for item in values', - 'ng-required': 'required' - }, true); - - - scope.$apply(function() { - scope.values = [{name: 'A', id: 1}, {name: 'B', id: 2}]; - scope.required = false; - }); - - element.val(''); - browserTrigger(element, 'change'); - expect(element).toBeValid(); - - scope.$apply(function() { - scope.required = true; - }); - expect(element).toBeInvalid(); - - scope.$apply(function() { - scope.value = scope.values[0]; - }); - expect(element).toBeValid(); - - element.val(''); - browserTrigger(element, 'change'); - expect(element).toBeInvalid(); - - scope.$apply(function() { - scope.required = false; - }); - expect(element).toBeValid(); - }); - - - it('should treat an empty array as invalid when `multiple` attribute used', function() { - createSelect({ - 'ng-model': 'value', - 'ng-options': 'item.name for item in values', - 'ng-required': 'required', - 'multiple': '' - }, true); - - scope.$apply(function() { - scope.value = []; - scope.values = [{name: 'A', id: 1}, {name: 'B', id: 2}]; - scope.required = true; - }); - expect(element).toBeInvalid(); - - scope.$apply(function() { - // ngModelWatch does not set objectEquality flag - // array must be replaced in order to trigger $formatters - scope.value = [scope.values[0]]; - }); - expect(element).toBeValid(); - }); - - - it('should allow falsy values as values', function() { - createSelect({ - 'ng-model': 'value', - 'ng-options': 'item.value as item.name for item in values', - 'ng-required': 'required' - }, true); - - scope.$apply(function() { - scope.values = [{name: 'True', value: true}, {name: 'False', value: false}]; - scope.required = false; - }); - - element.val('1'); - browserTrigger(element, 'change'); - expect(element).toBeValid(); - expect(scope.value).toBe(false); + expect(element).toBeValid(); + expect(element).toBePristine(); - scope.$apply(function() { - scope.required = true; - }); - expect(element).toBeValid(); - expect(scope.value).toBe(false); - }); + element[0].value = 'B'; + browserTrigger(element, 'change'); + expect(element).toBeValid(); + expect(element).toBeDirty(); }); - describe('ngModelCtrl', function() { - it('should prefix the model value with the word "the" using $parsers', function() { - createSelect({ - 'name': 'select', - 'ng-model': 'value', - 'ng-options': 'item for item in [\'first\', \'second\', \'third\', \'fourth\']' - }); - - scope.form.select.$parsers.push(function(value) { - return 'the ' + value; - }); - - element.val('2'); - browserTrigger(element, 'change'); - expect(scope.value).toBe('the third'); - expect(element.val()).toBe('2'); - }); - - it('should prefix the view value with the word "the" using $formatters', function() { - createSelect({ - 'name': 'select', - 'ng-model': 'value', - 'ng-options': 'item for item in [\'the first\', \'the second\', \'the third\', \'the fourth\']' - }); - - scope.form.select.$formatters.push(function(value) { - return 'the ' + value; - }); - - scope.$apply(function() { - scope.value = 'third'; - }); - expect(element.val()).toBe('2'); - }); - - it('should fail validation when $validators fail', function() { - createSelect({ - 'name': 'select', - 'ng-model': 'value', - 'ng-options': 'item for item in [\'first\', \'second\', \'third\', \'fourth\']' - }); - - scope.form.select.$validators.fail = function() { - return false; - }; - - element.val('2'); - browserTrigger(element, 'change'); - expect(element).toBeInvalid(); - expect(scope.value).toBeUndefined(); - expect(element.val()).toBe('2'); - }); - - it('should pass validation when $validators pass', function() { - createSelect({ - 'name': 'select', - 'ng-model': 'value', - 'ng-options': 'item for item in [\'first\', \'second\', \'third\', \'fourth\']' - }); - - scope.form.select.$validators.pass = function() { - return true; - }; - - element.val('2'); - browserTrigger(element, 'change'); - expect(element).toBeValid(); - expect(scope.value).toBe('third'); - expect(element.val()).toBe('2'); - }); - - it('should fail validation when $asyncValidators fail', inject(function($q, $rootScope) { - var defer; - createSelect({ - 'name': 'select', - 'ng-model': 'value', - 'ng-options': 'item for item in [\'first\', \'second\', \'third\', \'fourth\']' - }); - - scope.form.select.$asyncValidators.async = function() { - defer = $q.defer(); - return defer.promise; - }; - - element.val('2'); - browserTrigger(element, 'change'); - expect(scope.form.select.$pending).toBeDefined(); - expect(scope.value).toBeUndefined(); - expect(element.val()).toBe('2'); - - defer.reject(); - $rootScope.$digest(); - expect(scope.form.select.$pending).toBeUndefined(); - expect(scope.value).toBeUndefined(); - expect(element.val()).toBe('2'); - })); - - it('should pass validation when $asyncValidators pass', inject(function($q, $rootScope) { - var defer; - createSelect({ - 'name': 'select', - 'ng-model': 'value', - 'ng-options': 'item for item in [\'first\', \'second\', \'third\', \'fourth\']' - }); - - scope.form.select.$asyncValidators.async = function() { - defer = $q.defer(); - return defer.promise; - }; - - element.val('2'); - browserTrigger(element, 'change'); - expect(scope.form.select.$pending).toBeDefined(); - expect(scope.value).toBeUndefined(); - expect(element.val()).toBe('2'); - - defer.resolve(); - $rootScope.$digest(); - expect(scope.form.select.$pending).toBeUndefined(); - expect(scope.value).toBe('third'); - expect(element.val()).toBe('2'); - })); - }); }); @@ -2406,12 +831,12 @@ describe('select', function() { it('should populate value attribute on OPTION', function() { compile(''); - expect(element).toEqualSelect(['? undefined:undefined ?'], 'abc'); + expect(element).toEqualSelect([unknownValue(undefined)], 'abc'); }); it('should ignore value if already exists', function() { compile(''); - expect(element).toEqualSelect(['? undefined:undefined ?'], 'abc'); + expect(element).toEqualSelect([unknownValue(undefined)], 'abc'); }); it('should set value even if self closing HTML', function() {