'use strict'; describe('select', function() { var scope, formElement, element, $compile, ngModelCtrl, selectCtrl, renderSpy, optionAttributesList = []; function compile(html) { formElement = jqLite('
'); 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('