'use strict'; describe('select', function() { var scope, formElement, element, $compile, ngModelCtrl, selectCtrl, renderSpy, optionAttributesList = []; function compile(html) { formElement = jqLite('
' + html + '
'); element = formElement.find('select'); $compile(formElement)(scope); ngModelCtrl = element.controller('ngModel'); scope.$digest(); } function compileRepeatedOptions() { compile(''); } function compileGroupedOptions() { compile( ''); } function unknownValue(value) { return '? ' + hashKey(value) + ' ?'; } beforeEach(module(function($compileProvider) { $compileProvider.directive('spyOnWriteValue', function() { return { require: 'select', link: { pre: function(scope, element, attrs, ctrl) { selectCtrl = ctrl; renderSpy = jasmine.createSpy('renderSpy'); selectCtrl.ngModelCtrl.$render = renderSpy.and.callFake(selectCtrl.ngModelCtrl.$render); spyOn(selectCtrl, 'writeValue').and.callThrough(); } } }; }); $compileProvider.directive('myOptions', function() { return { scope: {myOptions: '='}, replace: true, template: '' }; }); $compileProvider.directive('exposeAttributes', function() { return { require: '^^select', link: { pre: function(scope, element, attrs, ctrl) { optionAttributesList.push(attrs); } } }; }); })); beforeEach(inject(function($rootScope, _$compile_) { scope = $rootScope.$new(); //create a child scope because the root scope can't be $destroy-ed $compile = _$compile_; formElement = element = null; })); afterEach(function() { scope.$destroy(); //disables unknown option work during destruction dealoc(formElement); ngModelCtrl = null; }); beforeEach(function() { jasmine.addMatchers({ toEqualSelectWithOptions: function() { return { compare: function(actual, expected) { var actualValues = {}; var optionGroup; var optionValue; forEach(actual.find('option'), function(option) { optionGroup = option.parentNode.label || ''; actualValues[optionGroup] = actualValues[optionGroup] || []; // IE9 doesn't populate the label property from the text property like other browsers optionValue = option.label || option.text; actualValues[optionGroup].push(option.selected ? [optionValue] : optionValue); }); var message = function() { return 'Expected ' + toJson(actualValues) + ' to equal ' + toJson(expected) + '.'; }; return { pass: equals(expected, actualValues), message: message }; } }; } }); }); it('should not add options to the select if ngModel is not present', inject(function($rootScope) { var scope = $rootScope; scope.d = 'd'; scope.e = 'e'; scope.f = 'f'; compile(''); var selectCtrl = element.controller('select'); expect(selectCtrl.hasOption('a')).toBe(false); expect(selectCtrl.hasOption('b')).toBe(false); expect(selectCtrl.hasOption('c')).toBe(false); expect(selectCtrl.hasOption('d')).toBe(false); expect(selectCtrl.hasOption('e')).toBe(false); expect(selectCtrl.hasOption('f')).toBe(false); })); describe('select-one', function() { it('should compile children of a select without a ngModel, but not create a model for it', function() { compile(''); scope.$apply(function() { scope.a = 'foo'; scope.b = 'bar'; }); expect(element.text()).toBe('foobarC'); }); it('should not interfere with selection via selected attr if ngModel directive is not present', function() { compile(''); expect(element).toEqualSelect('not me', ['me!'], 'nah'); }); describe('required state', function() { it('should set the error if the empty option is selected', function() { compile( ''); scope.$apply(function() { scope.selection = 'a'; }); expect(element).toBeValid(); expect(ngModelCtrl.$error.required).toBeFalsy(); var options = element.find('option'); // view -> model browserTrigger(options[0], 'click'); expect(element).toBeInvalid(); expect(ngModelCtrl.$error.required).toBeTruthy(); browserTrigger(options[1], 'click'); expect(element).toBeValid(); expect(ngModelCtrl.$error.required).toBeFalsy(); // model -> view scope.$apply('selection = null'); options = element.find('option'); expect(options[0]).toBeMarkedAsSelected(); expect(element).toBeInvalid(); expect(ngModelCtrl.$error.required).toBeTruthy(); }); it('should validate with empty option and bound ngRequired', function() { compile( ''); scope.$apply(function() { scope.required = false; }); var options = element.find('option'); browserTrigger(options[0], 'click'); expect(element).toBeValid(); scope.$apply('required = true'); expect(element).toBeInvalid(); scope.$apply('selection = "a"'); expect(element).toBeValid(); expect(element).toEqualSelect('', ['a'], 'b'); browserTrigger(options[0], 'click'); expect(element).toBeInvalid(); scope.$apply('required = false'); expect(element).toBeValid(); }); it('should not be invalid if no required attribute is present', function() { compile( ''); expect(element).toBeValid(); expect(element).toBePristine(); }); it('should NOT set the error if the unknown option is selected', function() { compile( ''); scope.$apply(function() { scope.selection = 'a'; }); expect(element).toBeValid(); expect(ngModelCtrl.$error.required).toBeFalsy(); scope.$apply('selection = "c"'); expect(element).toEqualSelect([unknownValue('c')], 'a', 'b'); expect(element).toBeValid(); expect(ngModelCtrl.$error.required).toBeFalsy(); }); }); it('should work with repeated value options', function() { scope.robots = ['c3p0', 'r2d2']; scope.robot = 'r2d2'; compile(''); expect(element).toEqualSelect('c3p0', ['r2d2']); browserTrigger(element.find('option').eq(0)); expect(element).toEqualSelect(['c3p0'], 'r2d2'); expect(scope.robot).toBe('c3p0'); scope.$apply(function() { scope.robots.unshift('wallee'); }); expect(element).toEqualSelect('wallee', ['c3p0'], 'r2d2'); expect(scope.robot).toBe('c3p0'); scope.$apply(function() { scope.robots = ['c3p0+', 'r2d2+']; scope.robot = 'r2d2+'; }); expect(element).toEqualSelect('c3p0+', ['r2d2+']); expect(scope.robot).toBe('r2d2+'); }); it('should interpolate select names', function() { scope.robots = ['c3p0', 'r2d2']; scope.name = 'r2d2'; scope.nameID = 47; compile(''); expect(scope.form.name47.$pristine).toBeTruthy(); browserTrigger(element.find('option').eq(0)); expect(scope.form.name47.$dirty).toBeTruthy(); expect(scope.name).toBe('c3p0'); }); it('should rename select controls in form when interpolated name changes', function() { scope.nameID = 'A'; compile(''); expect(scope.form.nameA.$name).toBe('nameA'); var oldModel = scope.form.nameA; scope.nameID = 'B'; scope.$digest(); expect(scope.form.nameA).toBeUndefined(); expect(scope.form.nameB).toBe(oldModel); expect(scope.form.nameB.$name).toBe('nameB'); }); it('should select options in a group when there is a linebreak before an option', function() { scope.mySelect = 'B'; scope.$apply(); var select = jqLite( ''); $compile(select)(scope); scope.$apply(); expect(select).toEqualSelectWithOptions({'first':['A'], 'second': [['B']]}); dealoc(select); }); it('should only call selectCtrl.writeValue after a digest has occurred', function() { scope.mySelect = 'B'; scope.$apply(); var select = jqLite( ''); $compile(select)(scope); expect(selectCtrl.writeValue).not.toHaveBeenCalled(); scope.$digest(); expect(selectCtrl.writeValue).toHaveBeenCalled(); dealoc(select); }); it('should remove the "selected" attribute from the previous option when the model changes', function() { compile(''); scope.$digest(); var options = element.find('option'); expect(options[0]).toBeMarkedAsSelected(); expect(options[1]).not.toBeMarkedAsSelected(); expect(options[2]).not.toBeMarkedAsSelected(); scope.selected = 'a'; scope.$digest(); options = element.find('option'); expect(options.length).toBe(3); expect(options[0]).not.toBeMarkedAsSelected(); expect(options[1]).toBeMarkedAsSelected(); expect(options[2]).not.toBeMarkedAsSelected(); scope.selected = 'b'; scope.$digest(); options = element.find('option'); expect(options[0]).not.toBeMarkedAsSelected(); expect(options[1]).not.toBeMarkedAsSelected(); expect(options[2]).toBeMarkedAsSelected(); // This will select the empty option scope.selected = null; scope.$digest(); expect(options[0]).toBeMarkedAsSelected(); expect(options[1]).not.toBeMarkedAsSelected(); expect(options[2]).not.toBeMarkedAsSelected(); // This will add and select the unknown option scope.selected = 'unmatched value'; scope.$digest(); options = element.find('option'); expect(options[0]).toBeMarkedAsSelected(); expect(options[1]).not.toBeMarkedAsSelected(); expect(options[2]).not.toBeMarkedAsSelected(); expect(options[3]).not.toBeMarkedAsSelected(); // Back to matched value scope.selected = 'b'; scope.$digest(); options = element.find('option'); expect(options[0]).not.toBeMarkedAsSelected(); expect(options[1]).not.toBeMarkedAsSelected(); expect(options[2]).toBeMarkedAsSelected(); }); describe('empty option', function() { it('should allow empty option to be added and removed dynamically', function() { scope.dynamicOptions = []; scope.robot = ''; compile(''); expect(element).toEqualSelect(['? string: ?']); scope.dynamicOptions = [ { val: '', display: '--empty--'}, { val: 'x', display: 'robot x' }, { val: 'y', display: 'robot y' } ]; scope.$digest(); expect(element).toEqualSelect([''], 'x', 'y'); scope.robot = 'x'; scope.$digest(); expect(element).toEqualSelect('', ['x'], 'y'); scope.dynamicOptions.shift(); scope.$digest(); expect(element).toEqualSelect(['x'], 'y'); scope.robot = undefined; scope.$digest(); expect(element).toEqualSelect([unknownValue(undefined)], 'x', 'y'); }); it('should cope use a dynamic empty option that is added to a static empty option', function() { // We do not make any special provisions for multiple empty options, so this behavior is // largely untested scope.dynamicOptions = []; scope.robot = 'x'; compile(''); scope.$digest(); expect(element).toEqualSelect([unknownValue('x')], ''); scope.robot = undefined; scope.$digest(); expect(element.find('option').eq(0).prop('selected')).toBe(true); expect(element.find('option').eq(0).text()).toBe('--static-select--'); scope.dynamicOptions = [ { val: '', display: '--dynamic-select--' }, { val: 'x', display: 'robot x' }, { val: 'y', display: 'robot y' } ]; scope.$digest(); expect(element).toEqualSelect('', [''], 'x', 'y'); scope.dynamicOptions = []; scope.$digest(); expect(element).toEqualSelect(['']); }); it('should select the empty option when model is undefined', function() { compile(''); expect(element).toEqualSelect([''], 'x', 'y'); }); it('should support defining an empty option anywhere in the option list', function() { compile(''); expect(element).toEqualSelect('x', [''], 'y'); }); it('should set the model to empty string when empty option is selected', function() { scope.robot = 'x'; compile(''); expect(element).toEqualSelect('', ['x'], 'y'); browserTrigger(element.find('option').eq(0)); expect(element).toEqualSelect([''], 'x', 'y'); expect(scope.robot).toBe(''); }); it('should remove unknown option when model is undefined', function() { scope.robot = 'other'; compile(''); expect(element).toEqualSelect([unknownValue('other')], '', 'x', 'y'); scope.robot = undefined; scope.$digest(); expect(element).toEqualSelect([''], 'x', 'y'); }); it('should support option without a value attribute', function() { compile(''); expect(element).toEqualSelect(['? undefined:undefined ?'], '--select--', 'x', 'y'); }); it('should support option without a value with other HTML attributes', function() { compile(''); expect(element).toEqualSelect(['? undefined:undefined ?'], '--select--', 'x', 'y'); }); describe('interactions with repeated options', function() { it('should select empty option when model is undefined', function() { scope.robots = ['c3p0', 'r2d2']; compile(''); expect(element).toEqualSelect([''], 'c3p0', 'r2d2'); }); it('should set model to empty string when selected', function() { scope.robots = ['c3p0', 'r2d2']; compile(''); browserTrigger(element.find('option').eq(1)); expect(element).toEqualSelect('', ['c3p0'], 'r2d2'); expect(scope.robot).toBe('c3p0'); browserTrigger(element.find('option').eq(0)); expect(element).toEqualSelect([''], 'c3p0', 'r2d2'); expect(scope.robot).toBe(''); }); it('should not break if both the select and repeater models change at once', function() { scope.robots = ['c3p0', 'r2d2']; scope.robot = 'c3p0'; compile(''); expect(element).toEqualSelect('', ['c3p0'], 'r2d2'); scope.$apply(function() { scope.robots = ['wallee']; scope.robot = ''; }); expect(element).toEqualSelect([''], 'wallee'); }); }); it('should add/remove the "selected" attribute when the empty option is selected/unselected', function() { compile(''); scope.$digest(); var options = element.find('option'); expect(options.length).toBe(3); expect(options[0]).toBeMarkedAsSelected(); expect(options[1]).not.toBeMarkedAsSelected(); expect(options[2]).not.toBeMarkedAsSelected(); scope.selected = 'a'; scope.$digest(); options = element.find('option'); expect(options.length).toBe(3); expect(options[0]).not.toBeMarkedAsSelected(); expect(options[1]).toBeMarkedAsSelected(); expect(options[2]).not.toBeMarkedAsSelected(); scope.selected = 'no match'; scope.$digest(); options = element.find('option'); expect(options[0]).toBeMarkedAsSelected(); expect(options[1]).not.toBeMarkedAsSelected(); expect(options[2]).not.toBeMarkedAsSelected(); }); }); describe('unknown option', function() { it('should insert&select temporary unknown option when no options-model match', function() { compile(''); expect(element).toEqualSelect([unknownValue(undefined)], 'c3p0', 'r2d2'); scope.$apply(function() { scope.robot = 'r2d2'; }); expect(element).toEqualSelect('c3p0', ['r2d2']); scope.$apply(function() { scope.robot = 'wallee'; }); expect(element).toEqualSelect([unknownValue('wallee')], 'c3p0', 'r2d2'); }); it('should NOT insert temporary unknown option when model is undefined and empty options ' + 'is present', function() { compile(''); expect(element).toEqualSelect([''], 'c3p0', 'r2d2'); expect(scope.robot).toBeUndefined(); scope.$apply(function() { scope.robot = null; }); expect(element).toEqualSelect([''], 'c3p0', 'r2d2'); scope.$apply(function() { scope.robot = 'r2d2'; }); expect(element).toEqualSelect('', 'c3p0', ['r2d2']); scope.$apply(function() { delete scope.robot; }); expect(element).toEqualSelect([''], 'c3p0', 'r2d2'); }); it('should insert&select temporary unknown option when no options-model match, empty ' + 'option is present and model is defined', function() { scope.robot = 'wallee'; compile(''); expect(element).toEqualSelect([unknownValue('wallee')], '', 'c3p0', 'r2d2'); scope.$apply(function() { scope.robot = 'r2d2'; }); expect(element).toEqualSelect('', 'c3p0', ['r2d2']); }); describe('interactions with repeated options', function() { it('should work with repeated options', function() { compile(''); expect(element).toEqualSelect([unknownValue(undefined)]); expect(scope.robot).toBeUndefined(); scope.$apply(function() { scope.robot = 'r2d2'; }); expect(element).toEqualSelect([unknownValue('r2d2')]); expect(scope.robot).toBe('r2d2'); scope.$apply(function() { scope.robots = ['c3p0', 'r2d2']; }); expect(element).toEqualSelect('c3p0', ['r2d2']); expect(scope.robot).toBe('r2d2'); }); it('should work with empty option and repeated options', function() { compile(''); expect(element).toEqualSelect(['']); expect(scope.robot).toBeUndefined(); scope.$apply(function() { scope.robot = 'r2d2'; }); expect(element).toEqualSelect([unknownValue('r2d2')], ''); expect(scope.robot).toBe('r2d2'); scope.$apply(function() { scope.robots = ['c3p0', 'r2d2']; }); expect(element).toEqualSelect('', 'c3p0', ['r2d2']); expect(scope.robot).toBe('r2d2'); }); it('should insert unknown element when repeater shrinks and selected option is unavailable', function() { scope.robots = ['c3p0', 'r2d2']; scope.robot = 'r2d2'; compile(''); expect(element).toEqualSelect('c3p0', ['r2d2']); expect(scope.robot).toBe('r2d2'); scope.$apply(function() { scope.robots.pop(); }); expect(element).toEqualSelect([unknownValue(null)], 'c3p0'); expect(scope.robot).toBe(null); scope.$apply(function() { scope.robots.unshift('r2d2'); }); expect(element).toEqualSelect([unknownValue(null)], 'r2d2', 'c3p0'); expect(scope.robot).toBe(null); scope.$apply(function() { scope.robot = 'r2d2'; }); expect(element).toEqualSelect(['r2d2'], 'c3p0'); scope.$apply(function() { delete scope.robots; }); expect(element).toEqualSelect([unknownValue(null)]); expect(scope.robot).toBe(null); }); }); }); it('should not break when adding options via a directive with `replace: true` ' + 'and a structural directive in its template', function() { scope.options = [ {value: '1', label: 'Option 1'}, {value: '2', label: 'Option 2'}, {value: '3', label: 'Option 3'} ]; compile(''); expect(element).toEqualSelect([unknownValue()], '1', '2', '3'); } ); it('should not throw when removing the element and all its children', function() { var template = ''; scope.visible = true; compile(template); // It should not throw when removing the element scope.$apply('visible = false'); }); }); describe('selectController', function() { it('should expose .$hasEmptyOption(), .$isEmptyOptionSelected(), ' + 'and .$isUnknownOptionSelected()', function() { compile(''); var selectCtrl = element.controller('select'); expect(selectCtrl.$hasEmptyOption).toEqual(jasmine.any(Function)); expect(selectCtrl.$isEmptyOptionSelected).toEqual(jasmine.any(Function)); expect(selectCtrl.$isUnknownOptionSelected).toEqual(jasmine.any(Function)); } ); it('should reflect the status of empty and unknown option', function() { scope.dynamicOptions = []; scope.selected = ''; compile(''); var selectCtrl = element.controller('select'); expect(element).toEqualSelect(['? string: ?']); expect(selectCtrl.$hasEmptyOption()).toBe(false); expect(selectCtrl.$isEmptyOptionSelected()).toBe(false); scope.dynamicOptions = [ { val: 'x', display: 'robot x' }, { val: 'y', display: 'robot y' } ]; scope.empty = true; scope.$digest(); expect(element).toEqualSelect([''], 'x', 'y'); expect(selectCtrl.$hasEmptyOption()).toBe(true); expect(selectCtrl.$isEmptyOptionSelected()).toBe(true); expect(selectCtrl.$isUnknownOptionSelected()).toBe(false); // empty -> selection scope.$apply('selected = "x"'); expect(element).toEqualSelect('', ['x'], 'y'); expect(selectCtrl.$hasEmptyOption()).toBe(true); expect(selectCtrl.$isEmptyOptionSelected()).toBe(false); expect(selectCtrl.$isUnknownOptionSelected()).toBe(false); // remove empty scope.$apply('empty = false'); expect(element).toEqualSelect(['x'], 'y'); expect(selectCtrl.$hasEmptyOption()).toBe(false); expect(selectCtrl.$isEmptyOptionSelected()).toBe(false); expect(selectCtrl.$isUnknownOptionSelected()).toBe(false); // selection -> unknown scope.$apply('selected = "unmatched"'); expect(element).toEqualSelect([unknownValue('unmatched')], 'x', 'y'); expect(selectCtrl.$hasEmptyOption()).toBe(false); expect(selectCtrl.$isEmptyOptionSelected()).toBe(false); expect(selectCtrl.$isUnknownOptionSelected()).toBe(true); // add empty scope.$apply('empty = true'); expect(element).toEqualSelect([unknownValue('unmatched')], '', 'x', 'y'); expect(selectCtrl.$hasEmptyOption()).toBe(true); expect(selectCtrl.$isEmptyOptionSelected()).toBe(false); expect(selectCtrl.$isUnknownOptionSelected()).toBe(true); // unknown -> empty scope.$apply('selected = null'); expect(element).toEqualSelect([''], 'x', 'y'); expect(selectCtrl.$hasEmptyOption()).toBe(true); expect(selectCtrl.$isEmptyOptionSelected()).toBe(true); expect(selectCtrl.$isUnknownOptionSelected()).toBe(false); // empty -> unknown scope.$apply('selected = "unmatched"'); expect(element).toEqualSelect([unknownValue('unmatched')], '', 'x', 'y'); expect(selectCtrl.$hasEmptyOption()).toBe(true); expect(selectCtrl.$isEmptyOptionSelected()).toBe(false); expect(selectCtrl.$isUnknownOptionSelected()).toBe(true); // unknown -> selection scope.$apply('selected = "y"'); expect(element).toEqualSelect('', 'x', ['y']); expect(selectCtrl.$hasEmptyOption()).toBe(true); expect(selectCtrl.$isEmptyOptionSelected()).toBe(false); expect(selectCtrl.$isUnknownOptionSelected()).toBe(false); // selection -> empty scope.$apply('selected = null'); expect(element).toEqualSelect([''], 'x', 'y'); expect(selectCtrl.$hasEmptyOption()).toBe(true); expect(selectCtrl.$isEmptyOptionSelected()).toBe(true); expect(selectCtrl.$isUnknownOptionSelected()).toBe(false); }); }); describe('selectController.hasOption', 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'} ]; compileRepeatedOptions(); var selectCtrl = element.controller('select'); scope.$apply(function() { scope.robots.shift(); }); expect(selectCtrl.hasOption('1')).toBe(false); expect(selectCtrl.hasOption('2')).toBe(true); }); it('should return false for options popped via ngRepeat', function() { scope.robots = [ {value: 1, label: 'c3p0'}, {value: 2, label: 'r2d2'} ]; compileRepeatedOptions(); var selectCtrl = element.controller('select'); scope.$apply(function() { scope.robots.pop(); }); expect(selectCtrl.hasOption('1')).toBe(true); expect(selectCtrl.hasOption('2')).toBe(false); }); it('should return true for options added via ngRepeat', function() { scope.robots = [ {value: 2, label: 'r2d2'} ]; compileRepeatedOptions(); var selectCtrl = element.controller('select'); scope.$apply(function() { scope.robots.unshift({value: 1, label: 'c3p0'}); }); expect(selectCtrl.hasOption('1')).toBe(true); expect(selectCtrl.hasOption('2')).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']]}); }); }); describe('grouped options', function() { 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(); var selectCtrl = element.controller('select'); scope.$apply(function() { var itemD = scope.groups[0].values.pop(); scope.groups[1].values.unshift(itemD); 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'}]; 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() { var itemD = scope.groups[1].values.shift(); scope.groups[0].values.push(itemD); 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'}]; scope.groups = [ { name: 'first', values: [ {name: 'B'}, {name: 'C'}, {name: 'D'} ] }, { name: 'second', values: [ {name: 'E'}, {name: 'F'} ] } ]; compileGroupedOptions(); var selectCtrl = element.controller('select'); scope.$apply(function() { var itemD = scope.groups[0].values.pop(); scope.groups[1].values.unshift(itemD); scope.groups[1].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'}]; 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.groups[0].values.pop(); var itemD = scope.groups[1].values.shift(); scope.groups[0].values.push(itemD); }); 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'}]; 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.groups[1].values.shift(); }); 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'}]; 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.groups.pop(); }); 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']; }); var optionElements = element.find('option'); expect(element).toEqualSelect(['A'], 'B'); expect(optionElements[0]).toBeMarkedAsSelected(); expect(optionElements[1]).not.toBeMarkedAsSelected(); scope.$apply(function() { scope.selection.push('B'); }); optionElements = element.find('option'); expect(element).toEqualSelect(['A'], ['B']); expect(optionElements[0]).toBeMarkedAsSelected(); expect(optionElements[1]).toBeMarkedAsSelected(); }); 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'); scope.$apply(function() { scope.selection.push('B'); }); expect(element).toEqualSelect(['A'], ['B']); }); it('should require', function() { 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('calls to $render', function() { var ngModelCtrl; beforeEach(function() { compile( ''); ngModelCtrl = element.controller('ngModel'); spyOn(ngModelCtrl, '$render').and.callThrough(); }); it('should call $render once when the reference to the viewValue changes', function() { scope.$apply(function() { scope.selection = ['A']; }); expect(ngModelCtrl.$render).toHaveBeenCalledTimes(1); scope.$apply(function() { scope.selection = ['A', 'B']; }); expect(ngModelCtrl.$render).toHaveBeenCalledTimes(2); scope.$apply(function() { scope.selection = []; }); expect(ngModelCtrl.$render).toHaveBeenCalledTimes(3); }); it('should call $render once when the viewValue deep-changes', function() { scope.$apply(function() { scope.selection = ['A']; }); expect(ngModelCtrl.$render).toHaveBeenCalledTimes(1); scope.$apply(function() { scope.selection.push('B'); }); expect(ngModelCtrl.$render).toHaveBeenCalledTimes(2); scope.$apply(function() { scope.selection.length = 0; }); expect(ngModelCtrl.$render).toHaveBeenCalledTimes(3); }); }); }); describe('option', function() { it('should populate a missing value attribute with the option text', function() { compile(''); expect(element).toEqualSelect([unknownValue(undefined)], 'abc'); }); it('should ignore the option text if the value attribute exists', function() { compile(''); expect(element).toEqualSelect([unknownValue(undefined)], 'abc'); }); it('should set value even if self closing HTML', function() { scope.x = 'hello'; compile(''); expect(element).toEqualSelect(['hello']); }); it('should add options with interpolated value attributes', function() { scope.option1 = 'option1'; scope.option2 = 'option2'; compile(''); scope.$digest(); expect(scope.selected).toBeUndefined(); browserTrigger(element.find('option').eq(0)); expect(scope.selected).toBe('option1'); scope.selected = 'option2'; scope.$digest(); expect(element.find('option').eq(1).prop('selected')).toBe(true); expect(element.find('option').eq(1).text()).toBe('Option 2'); }); it('should update the option when the interpolated value attribute changes', function() { scope.option1 = 'option1'; scope.option2 = ''; compile(''); var selectCtrl = element.controller('select'); spyOn(selectCtrl, 'removeOption').and.callThrough(); scope.$digest(); expect(scope.selected).toBeUndefined(); expect(selectCtrl.removeOption).not.toHaveBeenCalled(); //Change value of option2 scope.option2 = 'option2Changed'; scope.selected = 'option2Changed'; scope.$digest(); expect(selectCtrl.removeOption).toHaveBeenCalledWith(''); expect(element.find('option').eq(1).prop('selected')).toBe(true); expect(element.find('option').eq(1).text()).toBe('Option 2'); }); it('should add options with interpolated text', function() { scope.option1 = 'Option 1'; scope.option2 = 'Option 2'; compile(''); scope.$digest(); expect(scope.selected).toBeUndefined(); browserTrigger(element.find('option').eq(0)); expect(scope.selected).toBe('Option 1'); scope.selected = 'Option 2'; scope.$digest(); expect(element.find('option').eq(1).prop('selected')).toBe(true); expect(element.find('option').eq(1).text()).toBe('Option 2'); }); it('should update options when their interpolated text changes', function() { scope.option1 = 'Option 1'; scope.option2 = ''; compile(''); var selectCtrl = element.controller('select'); spyOn(selectCtrl, 'removeOption').and.callThrough(); scope.$digest(); expect(scope.selected).toBeUndefined(); expect(selectCtrl.removeOption).not.toHaveBeenCalled(); //Change value of option2 scope.option2 = 'Option 2 Changed'; scope.selected = 'Option 2 Changed'; scope.$digest(); expect(selectCtrl.removeOption).toHaveBeenCalledWith(''); expect(element.find('option').eq(1).prop('selected')).toBe(true); expect(element.find('option').eq(1).text()).toBe('Option 2 Changed'); }); it('should not blow up when option directive is found inside of a datalist', inject(function($compile, $rootScope) { var element = $compile('
' + '' + '{{foo}}' + '
')($rootScope); $rootScope.foo = 'success'; $rootScope.$digest(); expect(element.find('span').text()).toBe('success'); dealoc(element); })); it('should throw an exception if an option value interpolates to "hasOwnProperty"', function() { scope.hasOwnPropertyOption = 'hasOwnProperty'; expect(function() { compile(''); }).toThrowMinErr('ng','badname', 'hasOwnProperty is not a valid "option value" name'); }); describe('with ngValue (and non-primitive values)', function() { they('should set the option attribute and select it for value $prop', [ 'string', undefined, 1, true, null, {prop: 'value'}, ['a'], NaN ], function(prop) { scope.option1 = prop; scope.option2 = 'red'; scope.selected = 'NOMATCH'; compile(''); scope.$digest(); expect(element.find('option').eq(0).val()).toBe('? string:NOMATCH ?'); scope.selected = prop; scope.$digest(); expect(element.find('option').eq(0).val()).toBe(hashKey(prop)); // Reset scope.selected = false; scope.$digest(); expect(element.find('option').eq(0).val()).toBe('? boolean:false ?'); browserTrigger(element.find('option').eq(0)); if (isNumberNaN(prop)) { expect(scope.selected).toBeNaN(); } else { expect(scope.selected).toBe(prop); } }); they('should update the option attribute and select it for value $prop', [ 'string', undefined, 1, true, null, {prop: 'value'}, ['a'], NaN ], function(prop) { scope.option = prop; scope.option2 = 'red'; scope.selected = 'NOMATCH'; compile(''); var selectController = element.controller('select'); spyOn(selectController, 'removeOption').and.callThrough(); scope.$digest(); expect(selectController.removeOption).not.toHaveBeenCalled(); expect(element.find('option').eq(0).val()).toBe('? string:NOMATCH ?'); scope.selected = prop; scope.$digest(); expect(element.find('option').eq(0).val()).toBe(hashKey(prop)); expect(element[0].selectedIndex).toBe(0); scope.option = 'UPDATEDVALUE'; scope.$digest(); expect(selectController.removeOption.calls.count()).toBe(1); // Updating the option value currently does not update the select model if (isNumberNaN(prop)) { expect(selectController.removeOption.calls.argsFor(0)[0]).toBeNaN(); } else { expect(selectController.removeOption.calls.argsFor(0)[0]).toBe(prop); } expect(scope.selected).toBe(null); expect(element[0].selectedIndex).toBe(0); expect(element.find('option').length).toBe(3); expect(element.find('option').eq(0).prop('selected')).toBe(true); expect(element.find('option').eq(0).val()).toBe(unknownValue(prop)); expect(element.find('option').eq(1).prop('selected')).toBe(false); expect(element.find('option').eq(1).val()).toBe('string:UPDATEDVALUE'); scope.selected = 'UPDATEDVALUE'; scope.$digest(); expect(element[0].selectedIndex).toBe(0); expect(element.find('option').eq(0).val()).toBe('string:UPDATEDVALUE'); }); it('should interact with custom attribute $observe and $set calls', function() { var log = [], optionAttr; compile(''); optionAttr = optionAttributesList[0]; optionAttr.$observe('value', function(newVal) { log.push(newVal); }); scope.option = 'init'; scope.$digest(); expect(log[0]).toBe('init'); expect(element.find('option').eq(1).val()).toBe('string:init'); optionAttr.$set('value', 'update'); expect(log[1]).toBe('update'); expect(element.find('option').eq(1).val()).toBe('string:update'); }); it('should ignore the option text / value attribute if the ngValue attribute exists', function() { scope.ngvalue = 'abc'; scope.value = 'def'; scope.textvalue = 'ghi'; compile(''); expect(element).toEqualSelect([unknownValue(undefined)], 'string:abc'); }); it('should ignore option text with multiple interpolations if the ngValue attribute exists', function() { scope.ngvalue = 'abc'; scope.textvalue = 'def'; scope.textvalue2 = 'ghi'; compile(''); expect(element).toEqualSelect([unknownValue(undefined)], 'string:abc'); }); it('should select the first option if it is `undefined`', function() { scope.selected = undefined; scope.option1 = undefined; scope.option2 = 'red'; compile(''); expect(element).toEqualSelect(['undefined:undefined'], 'string:red'); }); describe('and select[multiple]', function() { it('should allow multiple selection', function() { scope.options = { a: 'string', b: undefined, c: 1, d: true, e: null, f: {prop: 'value'}, g: ['a'], h: NaN }; scope.selected = []; compile(''); scope.$digest(); expect(element).toEqualSelect( 'string:string', 'undefined:undefined', 'number:1', 'boolean:true', 'object:null', 'object:3', 'object:4', 'number:NaN' ); scope.selected = ['string', 1]; scope.$digest(); expect(element.find('option').eq(0).prop('selected')).toBe(true); expect(element.find('option').eq(2).prop('selected')).toBe(true); browserTrigger(element.find('option').eq(1)); expect(scope.selected).toEqual([undefined]); //reset scope.selected = []; scope.$digest(); forEach(element.find('option'), function(option) { // browserTrigger can't produce click + ctrl, so set selection manually jqLite(option).prop('selected', true); }); browserTrigger(element, 'change'); var arrayVal = ['a']; arrayVal.$$hashKey = 'object:4'; expect(scope.selected).toEqual([ 'string', undefined, 1, true, null, {prop: 'value', $$hashKey: 'object:3'}, arrayVal, NaN ]); }); }); }); describe('updating the model and selection when option elements are manipulated', function() { they('should set the model to null when the currently selected option with $prop is removed', ['ngValue', 'interpolatedValue', 'interpolatedText'], function(prop) { var A = { name: 'A'}, B = { name: 'B'}, C = { name: 'C'}; scope.options = [A, B, C]; scope.obj = {}; var optionString = ''; switch (prop) { case 'ngValue': optionString = ''; break; case 'interpolatedValue': optionString = ''; break; case 'interpolatedText': optionString = ''; break; } compile( '' ); var optionElements = element.find('option'); expect(optionElements.length).toEqual(4); browserTrigger(optionElements.eq(0)); optionElements = element.find('option'); expect(optionElements.length).toEqual(3); expect(scope.obj.value).toBe(prop === 'ngValue' ? A : 'A'); scope.options.shift(); scope.$digest(); optionElements = element.find('option'); expect(optionElements.length).toEqual(3); expect(scope.obj.value).toBe(null); expect(element.val()).toBe('? object:null ?'); }); they('should set the model to null when the currently selected option with $prop changes its value', [ 'ngValue', 'interpolatedValue', 'interpolatedText' ], function(prop) { var A = { name: 'A'}, B = { name: 'B'}, C = { name: 'C'}; scope.options = [A, B, C]; scope.obj = {}; var optionString = ''; switch (prop) { case 'ngValue': optionString = ''; break; case 'interpolatedValue': optionString = ''; break; case 'interpolatedText': optionString = ''; break; } compile( '' ); var optionElements = element.find('option'); expect(optionElements.length).toEqual(4); browserTrigger(optionElements.eq(0)); optionElements = element.find('option'); expect(optionElements.length).toEqual(3); expect(scope.obj.value).toBe('A'); A.name = 'X'; scope.$digest(); optionElements = element.find('option'); expect(optionElements.length).toEqual(4); expect(scope.obj.value).toBe(null); expect(element.val()).toBe('? string:A ?'); }); they('should set the model to null when the currently selected option with $prop is disabled', [ 'ngValue', 'interpolatedValue', 'interpolatedText' ], function(prop) { var A = { name: 'A'}, B = { name: 'B'}, C = { name: 'C'}; scope.options = [A, B, C]; scope.obj = {}; var optionString = ''; switch (prop) { case 'ngValue': optionString = ''; break; case 'interpolatedValue': optionString = ''; break; case 'interpolatedText': optionString = ''; break; } compile( '' ); var optionElements = element.find('option'); expect(optionElements.length).toEqual(4); browserTrigger(optionElements.eq(0)); optionElements = element.find('option'); expect(optionElements.length).toEqual(3); expect(scope.obj.value).toBe('A'); A.disabled = true; scope.$digest(); optionElements = element.find('option'); expect(optionElements.length).toEqual(4); expect(scope.obj.value).toBe(null); expect(element.val()).toBe('? object:null ?'); }); they('should select a disabled option with $prop when the model is set to the matching value', [ 'ngValue', 'interpolatedValue', 'interpolatedText' ], function(prop) { var A = { name: 'A'}, B = { name: 'B'}, C = { name: 'C'}; scope.options = [A, B, C]; scope.obj = {}; var optionString = ''; switch (prop) { case 'ngValue': optionString = ''; break; case 'interpolatedValue': optionString = ''; break; case 'interpolatedText': optionString = ''; break; } compile( '' ); var optionElements = element.find('option'); expect(optionElements.length).toEqual(4); expect(optionElements[0].value).toEqual(unknownValue(undefined)); B.disabled = true; scope.$digest(); optionElements = element.find('option'); expect(optionElements.length).toEqual(4); expect(optionElements[0].value).toEqual(unknownValue(undefined)); scope.obj.value = 'B'; scope.$digest(); optionElements = element.find('option'); expect(optionElements.length).toEqual(3); expect(scope.obj.value).toBe('B'); // jQuery returns null for val() when the option is disabled, see // https://bugs.jquery.com/ticket/13097 expect(element[0].value).toBe(prop === 'ngValue' ? 'string:B' : 'B'); expect(optionElements.eq(1).prop('selected')).toBe(true); }); they('should ignore an option with $prop that becomes enabled and does not match the model', [ 'ngValue', 'interpolatedValue', 'interpolatedText' ], function(prop) { var A = { name: 'A'}, B = { name: 'B'}, C = { name: 'C'}; scope.options = [A, B, C]; scope.obj = {}; var optionString = ''; switch (prop) { case 'ngValue': optionString = ''; break; case 'interpolatedValue': optionString = ''; break; case 'interpolatedText': optionString = ''; break; } compile( '' ); var optionElements = element.find('option'); expect(optionElements.length).toEqual(4); browserTrigger(optionElements.eq(0)); optionElements = element.find('option'); expect(optionElements.length).toEqual(3); expect(scope.obj.value).toBe('A'); A.disabled = true; scope.$digest(); optionElements = element.find('option'); expect(optionElements.length).toEqual(4); expect(scope.obj.value).toBe(null); expect(element.val()).toBe('? object:null ?'); A.disabled = false; scope.$digest(); optionElements = element.find('option'); expect(optionElements.length).toEqual(4); expect(scope.obj.value).toBe(null); expect(element.val()).toBe('? object:null ?'); }); they('should select a newly added option with $prop when it matches the current model', [ 'ngValue', 'interpolatedValue', 'interpolatedText' ], function(prop) { var A = { name: 'A'}, B = { name: 'B'}, C = { name: 'C'}; scope.options = [A, B]; scope.obj = { value: prop === 'ngValue' ? C : 'C' }; var optionString = ''; switch (prop) { case 'ngValue': optionString = ''; break; case 'interpolatedValue': optionString = ''; break; case 'interpolatedText': optionString = ''; break; } compile( '' ); var optionElements = element.find('option'); expect(optionElements.length).toEqual(3); scope.options.push(C); scope.$digest(); optionElements = element.find('option'); expect(element.val()).toBe(prop === 'ngValue' ? 'object:3' : 'C'); expect(optionElements.length).toEqual(3); expect(optionElements[2].selected).toBe(true); expect(scope.obj.value).toEqual(prop === 'ngValue' ? {name: 'C', $$hashKey: 'object:3'} : 'C'); }); they('should keep selection and model when repeated options with track by are replaced with equal options', [ 'ngValue', 'interpolatedValue', 'interpolatedText' ], function(prop) { var A = { name: 'A'}, B = { name: 'B'}, C = { name: 'C'}; scope.options = [A, B, C]; scope.obj = { value: 'C' }; var optionString = ''; switch (prop) { case 'ngValue': optionString = ''; break; case 'interpolatedValue': optionString = ''; break; case 'interpolatedText': optionString = ''; break; } compile( '' ); var optionElements = element.find('option'); expect(optionElements.length).toEqual(3); scope.obj.value = 'C'; scope.$digest(); optionElements = element.find('option'); expect(element.val()).toBe(prop === 'ngValue' ? 'string:C' : 'C'); expect(optionElements.length).toEqual(3); expect(optionElements[2].selected).toBe(true); expect(scope.obj.value).toBe('C'); scope.options = [ {name: 'A'}, {name: 'B'}, {name: 'C'} ]; scope.$digest(); optionElements = element.find('option'); expect(element.val()).toBe(prop === 'ngValue' ? 'string:C' : 'C'); expect(optionElements.length).toEqual(3); expect(optionElements[2].selected).toBe(true); expect(scope.obj.value).toBe('C'); }); describe('when multiple', function() { they('should set the model to null when the currently selected option with $prop is removed', [ 'ngValue', 'interpolatedValue', 'interpolatedText' ], function(prop) { var A = { name: 'A'}, B = { name: 'B'}, C = { name: 'C'}; scope.options = [A, B, C]; scope.obj = {}; var optionString = ''; switch (prop) { case 'ngValue': optionString = ''; break; case 'interpolatedValue': optionString = ''; break; case 'interpolatedText': optionString = ''; break; } compile( '' ); var ngModelCtrl = element.controller('ngModel'); var ngModelCtrlSpy = spyOn(ngModelCtrl, '$setViewValue').and.callThrough(); var optionElements = element.find('option'); expect(optionElements.length).toEqual(3); optionElements.eq(0).prop('selected', true); optionElements.eq(2).prop('selected', true); browserTrigger(element); optionElements = element.find('option'); expect(optionElements.length).toEqual(3); expect(scope.obj.value).toEqual(prop === 'ngValue' ? [A, C] : ['A', 'C']); ngModelCtrlSpy.calls.reset(); scope.options.shift(); scope.options.pop(); scope.$digest(); optionElements = element.find('option'); expect(optionElements.length).toEqual(1); expect(scope.obj.value).toEqual([]); // Cover both jQuery 3.x ([]) and 2.x (null) behavior. var val = element.val(); if (val === null) { val = []; } expect(val).toEqual([]); expect(ngModelCtrlSpy).toHaveBeenCalledTimes(1); }); they('should set the model to null when the currently selected option with $prop changes its value', [ 'ngValue', 'interpolatedValue', 'interpolatedText' ], function(prop) { var A = { name: 'A'}, B = { name: 'B'}, C = { name: 'C'}; scope.options = [A, B, C]; scope.obj = {}; var optionString = ''; switch (prop) { case 'ngValue': optionString = ''; break; case 'interpolatedValue': optionString = ''; break; case 'interpolatedText': optionString = ''; break; } compile( '' ); var ngModelCtrl = element.controller('ngModel'); var ngModelCtrlSpy = spyOn(ngModelCtrl, '$setViewValue').and.callThrough(); var optionElements = element.find('option'); expect(optionElements.length).toEqual(3); optionElements.eq(0).prop('selected', true); optionElements.eq(2).prop('selected', true); browserTrigger(element); optionElements = element.find('option'); expect(optionElements.length).toEqual(3); expect(scope.obj.value).toEqual(['A', 'C']); ngModelCtrlSpy.calls.reset(); A.name = 'X'; C.name = 'Z'; scope.$digest(); optionElements = element.find('option'); expect(optionElements.length).toEqual(3); expect(scope.obj.value).toEqual([]); // Cover both jQuery 3.x ([]) and 2.x (null) behavior. var val = element.val(); if (val === null) { val = []; } expect(val).toEqual([]); expect(ngModelCtrlSpy).toHaveBeenCalledTimes(1); }); they('should set the model to null when the currently selected option with $prop becomes disabled', [ 'ngValue', 'interpolatedValue', 'interpolatedText' ], function(prop) { var A = { name: 'A'}, B = { name: 'B'}, C = { name: 'C'}, D = { name: 'D'}; scope.options = [A, B, C, D]; scope.obj = {}; var optionString = ''; switch (prop) { case 'ngValue': optionString = ''; break; case 'interpolatedValue': optionString = ''; break; case 'interpolatedText': optionString = ''; break; } compile( '' ); var ngModelCtrl = element.controller('ngModel'); var ngModelCtrlSpy = spyOn(ngModelCtrl, '$setViewValue').and.callThrough(); var optionElements = element.find('option'); expect(optionElements.length).toEqual(4); optionElements.eq(0).prop('selected', true); optionElements.eq(2).prop('selected', true); optionElements.eq(3).prop('selected', true); browserTrigger(element); optionElements = element.find('option'); expect(optionElements.length).toEqual(4); expect(scope.obj.value).toEqual(['A', 'C', 'D']); ngModelCtrlSpy.calls.reset(); A.disabled = true; C.disabled = true; scope.$digest(); optionElements = element.find('option'); expect(optionElements.length).toEqual(4); expect(scope.obj.value).toEqual(['D']); expect(element.val()).toEqual(prop === 'ngValue' ? ['string:D'] : ['D']); expect(ngModelCtrlSpy).toHaveBeenCalledTimes(1); }); they('should select disabled options with $prop when the model is set to matching values', [ 'ngValue', 'interpolatedValue', 'interpolatedText' ], function(prop) { var A = { name: 'A'}, B = { name: 'B'}, C = { name: 'C'}, D = {name: 'D'}; scope.options = [A, B, C, D]; scope.obj = {}; var optionString = ''; switch (prop) { case 'ngValue': optionString = ''; break; case 'interpolatedValue': optionString = ''; break; case 'interpolatedText': optionString = ''; break; } compile( '' ); var optionElements = element.find('option'); expect(optionElements.length).toEqual(4); expect(element[0].value).toBe(''); A.disabled = true; D.disabled = true; scope.$digest(); optionElements = element.find('option'); expect(optionElements.length).toEqual(4); expect(element[0].value).toBe(''); scope.obj.value = prop === 'ngValue' ? [A, C, D] : ['A', 'C', 'D']; scope.$digest(); optionElements = element.find('option'); expect(optionElements.length).toEqual(4); expect(scope.obj.value).toEqual(prop === 'ngValue' ? [ {name: 'A', $$hashKey: 'object:3', disabled: true}, {name: 'C', $$hashKey: 'object:5'}, {name: 'D', $$hashKey: 'object:6', disabled: true} ] : ['A', 'C', 'D'] ); expect(optionElements.eq(0).prop('selected')).toBe(true); expect(optionElements.eq(2).prop('selected')).toBe(true); expect(optionElements.eq(3).prop('selected')).toBe(true); }); they('should select a newly added option with $prop when it matches the current model', [ 'ngValue', 'interpolatedValue', 'interpolatedText' ], function(prop) { var A = { name: 'A'}, B = { name: 'B'}, C = { name: 'C'}; scope.options = [A, B]; scope.obj = { value: prop === 'ngValue' ? [B, C] : ['B', 'C'] }; var optionString = ''; switch (prop) { case 'ngValue': optionString = ''; break; case 'interpolatedValue': optionString = ''; break; case 'interpolatedText': optionString = ''; break; } compile( '' ); var optionElements = element.find('option'); expect(optionElements.length).toEqual(2); expect(optionElements.eq(1).prop('selected')).toBe(true); scope.options.push(C); scope.$digest(); optionElements = element.find('option'); expect(element.val()).toEqual(prop === 'ngValue' ? ['object:4', 'object:7'] : ['B', 'C']); expect(optionElements.length).toEqual(3); expect(optionElements[1].selected).toBe(true); expect(optionElements[2].selected).toBe(true); expect(scope.obj.value).toEqual(prop === 'ngValue' ? [{ name: 'B', $$hashKey: 'object:4'}, {name: 'C', $$hashKey: 'object:7'}] : ['B', 'C']); }); they('should keep selection and model when a repeated options with track by are replaced with equal options', [ 'ngValue', 'interpolatedValue', 'interpolatedText' ], function(prop) { var A = { name: 'A'}, B = { name: 'B'}, C = { name: 'C'}; scope.options = [A, B, C]; scope.obj = { value: 'C' }; var optionString = ''; switch (prop) { case 'ngValue': optionString = ''; break; case 'interpolatedValue': optionString = ''; break; case 'interpolatedText': optionString = ''; break; } compile( '' ); var optionElements = element.find('option'); expect(optionElements.length).toEqual(3); scope.obj.value = ['B', 'C']; scope.$digest(); optionElements = element.find('option'); expect(element.val()).toEqual(prop === 'ngValue' ? ['string:B', 'string:C'] : ['B', 'C']); expect(optionElements.length).toEqual(3); expect(optionElements[1].selected).toBe(true); expect(optionElements[2].selected).toBe(true); expect(scope.obj.value).toEqual(['B', 'C']); scope.options = [ {name: 'A'}, {name: 'B'}, {name: 'C'} ]; scope.$digest(); optionElements = element.find('option'); expect(element.val()).toEqual(prop === 'ngValue' ? ['string:B', 'string:C'] : ['B', 'C']); expect(optionElements.length).toEqual(3); expect(optionElements[1].selected).toBe(true); expect(optionElements[2].selected).toBe(true); expect(scope.obj.value).toEqual(['B', 'C']); }); }); it('should keep the ngModel value when the selected option is recreated by ngRepeat', function() { scope.options = [{ name: 'A'}, { name: 'B'}, { name: 'C'}]; scope.obj = { value: 'B' }; compile( '' ); var optionElements = element.find('option'); expect(optionElements.length).toEqual(3); expect(optionElements[0].value).toBe('A'); expect(optionElements[1]).toBeMarkedAsSelected(); expect(scope.obj.value).toBe('B'); scope.$apply(function() { // Only when new objects are used, ngRepeat re-creates the element from scratch scope.options = [{ name: 'B'}, { name: 'C'}, { name: 'D'}]; }); var previouslySelectedOptionElement = optionElements[1]; optionElements = element.find('option'); expect(optionElements.length).toEqual(3); expect(optionElements[0].value).toBe('B'); expect(optionElements[0]).toBeMarkedAsSelected(); expect(scope.obj.value).toBe('B'); // Ensure the assumption that the element is re-created is true expect(previouslySelectedOptionElement).not.toBe(optionElements[0]); }); it('should validate when the options change', function() { scope.values = ['A', 'B']; scope.selection = 'A'; compile( '' ); expect(element).toEqualSelect('', ['A'], 'B'); expect(element).toBeValid(); expect(ngModelCtrl.$error.required).toBeFalsy(); scope.$apply(function() { // Only when new objects are used, ngRepeat re-creates the element from scratch scope.values = ['B', 'C']; }); expect(element).toEqualSelect([''], 'B', 'C'); expect(element).toBeInvalid(); expect(ngModelCtrl.$error.required).toBeTruthy(); // ngModel sets undefined for invalid values expect(scope.selection).toBeUndefined(); }); }); }); });