Files
angular.js/test/ng/directive/ngOptionsSpec.js
T
2018-03-16 16:49:57 +01:00

3501 lines
108 KiB
JavaScript

'use strict';
describe('ngOptions', function() {
var scope, formElement, element, $compile, linkLog, childListMutationObserver, ngModelCtrl;
function compile(html) {
formElement = jqLite('<form name="form">' + html + '</form>');
element = formElement.find('select');
$compile(formElement)(scope);
ngModelCtrl = element.controller('ngModel');
scope.$apply();
}
function setSelectValue(selectElement, optionIndex) {
var option = selectElement.find('option').eq(optionIndex);
selectElement.val(option.val());
browserTrigger(element, 'change');
}
beforeEach(function() {
jasmine.addMatchers({
toEqualSelectValue: function() {
return {
compare: function(_actual_, value, multiple) {
var errors = [];
var actual = _actual_.val();
if (multiple) {
value = value.map(function(val) { return hashKey(val); });
actual = actual || [];
} else {
value = hashKey(value);
}
if (!equals(actual, value)) {
errors.push('Expected select value "' + actual + '" to equal "' + value + '"');
}
var message = function() {
return errors.join('\n');
};
return { pass: errors.length === 0, message: message };
}
};
},
toEqualOption: function() {
return {
compare: function(actual, value, text, label) {
var errors = [];
var hash = hashKey(value);
if (actual.attr('value') !== hash) {
errors.push('Expected option value "' + actual.attr('value') + '" to equal "' + hash + '"');
}
if (text && actual.text() !== text) {
errors.push('Expected option text "' + actual.text() + '" to equal "' + text + '"');
}
if (label && actual.attr('label') !== label) {
errors.push('Expected option label "' + actual.attr('label') + '" to equal "' + label + '"');
}
var message = function() {
return errors.join('\n');
};
return { pass: errors.length === 0, message: message };
}
};
},
toEqualTrackedOption: function() {
return {
compare: function(actual, value, text, label) {
var errors = [];
if (actual.attr('value') !== '' + value) {
errors.push('Expected option value "' + actual.attr('value') + '" to equal "' + value + '"');
}
if (text && actual.text() !== text) {
errors.push('Expected option text "' + actual.text() + '" to equal "' + text + '"');
}
if (label && actual.attr('label') !== label) {
errors.push('Expected option label "' + actual.attr('label') + '" to equal "' + label + '"');
}
var message = function() {
return errors.join('\n');
};
return { pass: errors.length === 0, message: message };
}
};
},
toEqualUnknownOption: function() {
return {
compare: function(actual) {
var errors = [];
if (actual.attr('value') !== '?') {
errors.push('Expected option value "' + actual.attr('value') + '" to equal "?"');
}
var message = function() {
return errors.join('\n');
};
return { pass: errors.length === 0, message: message };
}
};
},
toEqualUnknownValue: function() {
return {
compare: function(actual, value) {
var errors = [];
if (actual !== '?') {
errors.push('Expected select value "' + actual + '" to equal "?"');
}
var message = function() {
return errors.join('\n');
};
return { pass: errors.length === 0, message: message };
}
};
}
});
});
beforeEach(module(function($compileProvider, $provide) {
linkLog = [];
$compileProvider
.directive('customSelect', function() {
return {
restrict: 'E',
replace: true,
scope: {
ngModel: '=',
options: '='
},
templateUrl: 'select_template.html',
link: function(scope, $element, attributes) {
scope.selectable_options = scope.options;
}
};
})
.directive('oCompileContents', function() {
return {
link: function(scope, element) {
linkLog.push('linkCompileContents');
$compile(element.contents())(scope);
}
};
})
.directive('observeChildList', function() {
return {
link: function(scope, element) {
var config = { childList: true };
childListMutationObserver = new window.MutationObserver(noop);
childListMutationObserver.observe(element[0], config);
}
};
});
$provide.decorator('ngOptionsDirective', function($delegate) {
var origPreLink = $delegate[0].link.pre;
var origPostLink = $delegate[0].link.post;
$delegate[0].compile = function() {
return {
pre: origPreLink,
post: function() {
linkLog.push('linkNgOptions');
origPostLink.apply(this, arguments);
}
};
};
return $delegate;
});
}));
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;
});
function createSelect(attrs, blank, unknown) {
var html = '<select';
forEach(attrs, function(value, key) {
if (isBoolean(value)) {
if (value) html += ' ' + key;
} else {
html += ' ' + key + '="' + value + '"';
}
});
html += '>' +
(blank ? (isString(blank) ? blank : '<option value="">blank</option>') : '') +
(unknown ? (isString(unknown) ? unknown : '<option value="?">unknown</option>') : '') +
'</select>';
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 throw when not formated "? for ? in ?"', function() {
expect(function() {
compile('<select ng-model="selected" ng-options="i dont parse"></select>');
}).toThrowMinErr('ngOptions', 'iexp', /Expected expression in form of/);
});
it('should have a dependency on ngModel', function() {
expect(function() {
compile('<select ng-options="item in items"></select>');
}).toThrow();
});
it('should render a list', function() {
createSingleSelect();
scope.$apply(function() {
scope.values = [{name: 'A'}, {name: 'B'}, {name: 'C'}];
scope.selected = scope.values[1];
});
var options = element.find('option');
expect(options.length).toEqual(3);
expect(options.eq(0)).toEqualOption(scope.values[0], 'A');
expect(options.eq(1)).toEqualOption(scope.values[1], 'B');
expect(options.eq(2)).toEqualOption(scope.values[2], 'C');
expect(options[1].selected).toEqual(true);
});
it('should not include properties with non-numeric keys in array-like collections when using array syntax', function() {
createSelect({
'ng-model':'selected',
'ng-options':'value for value in values'
});
scope.$apply(function() {
scope.values = { 0: 'X', 1: 'Y', 2: 'Z', 'a': 'A', length: 3};
scope.selected = scope.values[1];
});
var options = element.find('option');
expect(options.length).toEqual(3);
expect(options.eq(0)).toEqualOption('X');
expect(options.eq(1)).toEqualOption('Y');
expect(options.eq(2)).toEqualOption('Z');
});
it('should include properties with non-numeric keys in array-like collections when using object syntax', function() {
createSelect({
'ng-model':'selected',
'ng-options':'value for (key, value) in values'
});
scope.$apply(function() {
scope.values = { 0: 'X', 1: 'Y', 2: 'Z', 'a': 'A', length: 3};
scope.selected = scope.values[1];
});
var options = element.find('option');
expect(options.length).toEqual(5);
expect(options.eq(0)).toEqualOption('X');
expect(options.eq(1)).toEqualOption('Y');
expect(options.eq(2)).toEqualOption('Z');
expect(options.eq(3)).toEqualOption('A');
expect(options.eq(4)).toEqualOption(3);
});
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.green;
});
var options = element.find('option');
expect(options.length).toEqual(3);
expect(options.eq(0)).toEqualOption('FF0000', 'red');
expect(options.eq(1)).toEqualOption('00FF00', 'green');
expect(options.eq(2)).toEqualOption('0000FF', 'blue');
expect(options[1].selected).toEqual(true);
scope.$apply('object.azur = "8888FF"');
options = element.find('option');
expect(options[1].selected).toEqual(true);
scope.$apply('selected = object.azur');
options = element.find('option');
expect(options[3].selected).toEqual(true);
});
it('should set the "selected" attribute and property on selected options', function() {
scope.values = [{
id: 'FF0000',
display: 'red'
}, {
id: '0000FF',
display: 'blue'
}];
scope.selected = 'FF0000';
createSelect({
'ng-model': 'selected',
'ng-options': 'option.id as option.display for option in values'
});
scope.$digest();
var options = element.find('option');
expect(options.length).toEqual(2);
expect(options.eq(0)).toEqualOption('FF0000', 'red');
expect(options.eq(1)).toEqualOption('0000FF', 'blue');
expect(options.eq(0)[0].getAttribute('selected')).toBe('selected');
expect(options.eq(0).attr('selected')).toBe('selected');
expect(options.eq(0)[0].selected).toBe(true);
expect(options.eq(0).prop('selected')).toBe(true);
scope.selected = '0000FF';
scope.$digest();
expect(options.eq(1)[0].getAttribute('selected')).toBe('selected');
expect(options.eq(1).attr('selected')).toBe('selected');
expect(options.eq(1)[0].selected).toBe(true);
expect(options.eq(1).prop('selected')).toBe(true);
});
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(scope.values[0], '0');
expect(options.eq(1)).toEqualOption(scope.values[1], '1');
expect(options.eq(2)).toEqualOption(scope.values[2], '2');
});
it('should not be set when an option is selected and options are set asynchronously',
inject(function($timeout) {
compile('<select ng-model="model" ng-options="opt.id as opt.label for opt in options">' +
'</select>');
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');
})
);
it('should grow list', function() {
createSingleSelect();
scope.$apply(function() {
scope.values = [];
});
expect(element.find('option').length).toEqual(1); // because we add special unknown option
expect(element.find('option').eq(0)).toEqualUnknownOption();
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(scope.values[0], 'A');
scope.$apply(function() {
scope.values.push({name:'B'});
});
expect(element.find('option').length).toEqual(2);
expect(element.find('option').eq(0)).toEqualOption(scope.values[0], 'A');
expect(element.find('option').eq(1)).toEqualOption(scope.values[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(scope.values[0], 'A');
expect(element.find('option').eq(1)).toEqualOption(scope.values[1], 'B');
scope.$apply(function() {
scope.values.pop();
});
expect(element.find('option').length).toEqual(1);
expect(element.find('option')).toEqualOption(scope.values[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(scope.values[0], 'B');
expect(options.eq(1)).toEqualOption(scope.values[1], 'C');
expect(options.eq(2)).toEqualOption(scope.values[2], 'D');
});
it('should preserve pre-existing empty option', 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 not watch non-numeric array properties', function() {
createSelect({
'ng-options': 'value as createLabel(value) for value in array',
'ng-model': 'selected'
});
scope.createLabel = jasmine.createSpy('createLabel').and.callFake(function(value) { return value; });
scope.array = ['a', 'b', 'c'];
scope.array.$$private = 'do not watch';
scope.array.$property = 'do not watch';
scope.array.other = 'do not watch';
scope.array.fn = function() {};
scope.selected = 'b';
scope.$digest();
expect(scope.createLabel).toHaveBeenCalledWith('a');
expect(scope.createLabel).toHaveBeenCalledWith('b');
expect(scope.createLabel).toHaveBeenCalledWith('c');
expect(scope.createLabel).not.toHaveBeenCalledWith('do not watch');
expect(scope.createLabel).not.toHaveBeenCalledWith(jasmine.any(Function));
});
it('should not watch object properties that start with $ or $$', function() {
createSelect({
'ng-options': 'key as createLabel(key) for (key, value) in object',
'ng-model': 'selected'
});
scope.createLabel = jasmine.createSpy('createLabel').and.callFake(function(value) { return value; });
scope.object = {'regularProperty': 'visible', '$$private': 'invisible', '$property': 'invisible'};
scope.selected = 'regularProperty';
scope.$digest();
expect(scope.createLabel).toHaveBeenCalledWith('regularProperty');
expect(scope.createLabel).not.toHaveBeenCalledWith('$$private');
expect(scope.createLabel).not.toHaveBeenCalledWith('$property');
});
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 values\n' +
'| filter:isNotFoo',
'ng-model': 'selected'
});
scope.$apply(function() {
scope.values = [{'id': 1, 'name': 'Foo'},
{'id': 2, 'name': 'Bar'},
{'id': 3, 'name': 'Baz'}];
scope.selected = scope.values[0];
});
var options = element.find('option');
expect(options.length).toEqual(3);
expect(options.eq(1)).toEqualOption(scope.values[1], '2');
expect(options.eq(2)).toEqualOption(scope.values[2], '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(jasmine.objectContaining({ 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');
});
it('should update the label if only the property has changed', 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');
scope.$apply('values[0].name = "X"');
options = element.find('option');
expect(options.eq(0).prop('label')).toEqual('X');
});
// bug fix #9714
it('should select the matching option when the options are updated', function() {
// first set up a select with no options
scope.selected = '';
createSelect({
'ng-options': 'val.id as val.label for val in values',
'ng-model': 'selected'
});
var options = element.find('option');
// we expect the selected option to be the "unknown" option
expect(options.eq(0)).toEqualUnknownOption('');
expect(options.eq(0).prop('selected')).toEqual(true);
// now add some real options - one of which matches the selected value
scope.$apply('values = [{id:"",label:"A"},{id:"1",label:"B"},{id:"2",label:"C"}]');
// we expect the selected option to be the one that matches the correct item
// and for the unknown option to have been removed
options = element.find('option');
expect(element).toEqualSelectValue('');
expect(options.eq(0)).toEqualOption('','A');
});
it('should be possible to use one-time binding on the expression', function() {
createSelect({
'ng-model': 'someModel',
'ng-options': 'o as o for o in ::arr'
});
var options;
// Initially the options list is just the unknown option
options = element.find('option');
expect(options.length).toEqual(1);
// Now initialize the scope and the options should be updated
scope.$apply(function() {
scope.arr = ['a','b','c'];
});
options = element.find('option');
expect(options.length).toEqual(4);
expect(options.eq(0)).toEqualUnknownOption();
expect(options.eq(1)).toEqualOption('a');
expect(options.eq(2)).toEqualOption('b');
expect(options.eq(3)).toEqualOption('c');
// Change the scope but the options should not change
scope.arr = ['w', 'x', 'y', 'z'];
scope.$digest();
options = element.find('option');
expect(options.length).toEqual(4);
expect(options.eq(0)).toEqualUnknownOption();
expect(options.eq(1)).toEqualOption('a');
expect(options.eq(2)).toEqualOption('b');
expect(options.eq(3)).toEqualOption('c');
});
it('should remove the "selected" attribute from the previous option when the model changes', function() {
scope.values = [{id: 10, label: 'ten'}, {id:20, label: 'twenty'}];
createSelect({
'ng-model': 'selected',
'ng-options': 'item.label for item in values'
}, true);
var options = element.find('option');
expect(options[0]).toBeMarkedAsSelected();
expect(options[1]).not.toBeMarkedAsSelected();
expect(options[2]).not.toBeMarkedAsSelected();
scope.selected = scope.values[0];
scope.$digest();
expect(options[0]).not.toBeMarkedAsSelected();
expect(options[1]).toBeMarkedAsSelected();
expect(options[2]).not.toBeMarkedAsSelected();
scope.selected = scope.values[1];
scope.$digest();
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 = scope.values[1];
scope.$digest();
options = element.find('option');
expect(options[0]).not.toBeMarkedAsSelected();
expect(options[1]).not.toBeMarkedAsSelected();
expect(options[2]).toBeMarkedAsSelected();
});
if (window.MutationObserver) {
//IE9 and IE10 do not support MutationObserver
//Since the feature is only needed for a test, it's okay to skip these browsers
it('should render the initial options only one time', function() {
scope.value = 'black';
scope.values = ['black', 'white', 'red'];
// observe-child-list adds a MutationObserver that we will read out after ngOptions
// has been compiled
createSelect({
'ng-model':'value',
'ng-options':'value.name for value in values',
'observe-child-list': ''
});
var optionEls = element[0].querySelectorAll('option');
var records = childListMutationObserver.takeRecords();
expect(records.length).toBe(1);
expect(records[0].addedNodes).toEqual(optionEls);
});
}
describe('disableWhen expression', function() {
describe('on single select', function() {
it('should disable options', function() {
scope.selected = '';
scope.options = [
{ name: 'white', value: '#FFFFFF' },
{ name: 'one', value: 1, unavailable: true },
{ name: 'notTrue', value: false },
{ name: 'thirty', value: 30, unavailable: false }
];
createSelect({
'ng-options': 'o.value as o.name disable when o.unavailable for o in options',
'ng-model': 'selected'
});
var options = element.find('option');
expect(options.length).toEqual(5);
expect(options.eq(1).prop('disabled')).toEqual(false);
expect(options.eq(2).prop('disabled')).toEqual(true);
expect(options.eq(3).prop('disabled')).toEqual(false);
expect(options.eq(4).prop('disabled')).toEqual(false);
});
it('should select disabled options when model changes', function() {
scope.options = [
{ name: 'white', value: '#FFFFFF' },
{ name: 'one', value: 1, unavailable: true },
{ name: 'notTrue', value: false },
{ name: 'thirty', value: 30, unavailable: false }
];
createSelect({
'ng-options': 'o.value as o.name disable when o.unavailable for o in options',
'ng-model': 'selected'
});
// Initially the model is set to an enabled option
scope.$apply('selected = 30');
var options = element.find('option');
expect(options.eq(3).prop('selected')).toEqual(true);
// Now set the model to a disabled option
scope.$apply('selected = 1');
options = element.find('option');
// jQuery returns null for val() when the option is disabled, see
// https://bugs.jquery.com/ticket/13097
expect(element[0].value).toBe('number:1');
expect(options.length).toEqual(4);
expect(options.eq(0).prop('selected')).toEqual(false);
expect(options.eq(1).prop('selected')).toEqual(true);
expect(options.eq(2).prop('selected')).toEqual(false);
expect(options.eq(3).prop('selected')).toEqual(false);
});
it('should select options in model when they become enabled', function() {
scope.options = [
{ name: 'white', value: '#FFFFFF' },
{ name: 'one', value: 1, unavailable: true },
{ name: 'notTrue', value: false },
{ name: 'thirty', value: 30, unavailable: false }
];
createSelect({
'ng-options': 'o.value as o.name disable when o.unavailable for o in options',
'ng-model': 'selected'
});
// Set the model to a disabled option
scope.$apply('selected = 1');
var options = element.find('option');
// jQuery returns null for val() when the option is disabled, see
// https://bugs.jquery.com/ticket/13097
expect(element[0].value).toBe('number:1');
expect(options.length).toEqual(4);
expect(options.eq(0).prop('selected')).toEqual(false);
expect(options.eq(1).prop('selected')).toEqual(true);
expect(options.eq(2).prop('selected')).toEqual(false);
expect(options.eq(3).prop('selected')).toEqual(false);
// Now enable that option
scope.$apply(function() {
scope.options[1].unavailable = false;
});
expect(element).toEqualSelectValue(1);
options = element.find('option');
expect(options.length).toEqual(4);
expect(options.eq(1).prop('selected')).toEqual(true);
expect(options.eq(3).prop('selected')).toEqual(false);
});
});
describe('on multi select', function() {
it('should disable options', function() {
scope.selected = [];
scope.options = [
{ name: 'a', value: 0 },
{ name: 'b', value: 1, unavailable: true },
{ name: 'c', value: 2 },
{ name: 'd', value: 3, unavailable: false }
];
createSelect({
'ng-options': 'o.value as o.name disable when o.unavailable for o in options',
'multiple': true,
'ng-model': 'selected'
});
var options = element.find('option');
expect(options.eq(0).prop('disabled')).toEqual(false);
expect(options.eq(1).prop('disabled')).toEqual(true);
expect(options.eq(2).prop('disabled')).toEqual(false);
expect(options.eq(3).prop('disabled')).toEqual(false);
});
it('should select disabled options when model changes', function() {
scope.options = [
{ name: 'a', value: 0 },
{ name: 'b', value: 1, unavailable: true },
{ name: 'c', value: 2 },
{ name: 'd', value: 3, unavailable: false }
];
createSelect({
'ng-options': 'o.value as o.name disable when o.unavailable for o in options',
'multiple': true,
'ng-model': 'selected'
});
// Initially the model is set to an enabled option
scope.$apply('selected = [3]');
var options = element.find('option');
expect(options.eq(0).prop('selected')).toEqual(false);
expect(options.eq(1).prop('selected')).toEqual(false);
expect(options.eq(2).prop('selected')).toEqual(false);
expect(options.eq(3).prop('selected')).toEqual(true);
// Now add a disabled option
scope.$apply('selected = [1,3]');
options = element.find('option');
expect(options.eq(0).prop('selected')).toEqual(false);
expect(options.eq(1).prop('selected')).toEqual(true);
expect(options.eq(2).prop('selected')).toEqual(false);
expect(options.eq(3).prop('selected')).toEqual(true);
// Now only select the disabled option
scope.$apply('selected = [1]');
expect(options.eq(0).prop('selected')).toEqual(false);
expect(options.eq(1).prop('selected')).toEqual(true);
expect(options.eq(2).prop('selected')).toEqual(false);
expect(options.eq(3).prop('selected')).toEqual(false);
});
it('should select options in model when they become enabled', function() {
scope.options = [
{ name: 'a', value: 0 },
{ name: 'b', value: 1, unavailable: true },
{ name: 'c', value: 2 },
{ name: 'd', value: 3, unavailable: false }
];
createSelect({
'ng-options': 'o.value as o.name disable when o.unavailable for o in options',
'multiple': true,
'ng-model': 'selected'
});
// Set the model to a disabled option
scope.$apply('selected = [1]');
var options = element.find('option');
expect(options.eq(0).prop('selected')).toEqual(false);
expect(options.eq(1).prop('selected')).toEqual(true);
expect(options.eq(2).prop('selected')).toEqual(false);
expect(options.eq(3).prop('selected')).toEqual(false);
// Now enable that option
scope.$apply(function() {
scope.options[1].unavailable = false;
});
expect(element).toEqualSelectValue([1], true);
options = element.find('option');
expect(options.eq(0).prop('selected')).toEqual(false);
expect(options.eq(1).prop('selected')).toEqual(true);
expect(options.eq(2).prop('selected')).toEqual(false);
expect(options.eq(3).prop('selected')).toEqual(false);
});
});
});
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'}};
});
it('should support single select with array source', function() {
createSelect({
'ng-model': 'selected',
'ng-options': 'item.id as item.label for item in arr'
});
scope.$apply(function() {
scope.selected = 10;
});
expect(element).toEqualSelectValue(10);
setSelectValue(element, 1);
expect(scope.selected).toBe(20);
});
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).toEqualSelectValue([10,20], true);
expect(scope.selected).toEqual([10,20]);
element.children()[0].selected = false;
browserTrigger(element, 'change');
expect(scope.selected).toEqual([20]);
expect(element).toEqualSelectValue([20], true);
});
it('should re-render if an item in an array source is added/removed', function() {
createSelect({
'ng-model': 'selected',
'multiple': true,
'ng-options': 'item.id as item.label for item in arr'
});
scope.$apply(function() {
scope.selected = [10];
});
expect(element).toEqualSelectValue([10], true);
scope.$apply(function() {
scope.selected.push(20);
});
expect(element).toEqualSelectValue([10, 20], true);
scope.$apply(function() {
scope.selected.shift();
});
expect(element).toEqualSelectValue([20], true);
});
it('should handle a options containing circular references', function() {
scope.arr[0].ref = scope.arr[0];
scope.selected = [scope.arr[0]];
createSelect({
'ng-model': 'selected',
'multiple': true,
'ng-options': 'item as item.label for item in arr'
});
expect(element).toEqualSelectValue([scope.arr[0]], true);
scope.$apply(function() {
scope.selected.push(scope.arr[1]);
});
expect(element).toEqualSelectValue([scope.arr[0], scope.arr[1]], true);
scope.$apply(function() {
scope.selected.pop();
});
expect(element).toEqualSelectValue([scope.arr[0]], true);
});
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'
});
scope.$apply(function() {
scope.selected = 10;
});
expect(element).toEqualSelectValue(10);
setSelectValue(element, 1);
expect(scope.selected).toBe(20);
});
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'
});
scope.$apply(function() {
scope.selected = [10,20];
});
expect(element).toEqualSelectValue([10,20], true);
element.children()[0].selected = false;
browserTrigger(element, 'change');
expect(scope.selected).toEqual([20]);
expect(element).toEqualSelectValue([20], true);
});
});
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 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'
});
expect(element.val()).toEqualUnknownValue();
scope.$apply(function() {
scope.selected = scope.arr[0];
});
expect(element.val()).toBe('10');
scope.$apply(function() {
scope.arr[0] = {id: 10, label: 'new ten'};
});
expect(element.val()).toBe('10');
element.children()[1].selected = 'selected';
browserTrigger(element, 'change');
expect(scope.selected).toEqual(scope.arr[1]);
});
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)).toEqualUnknownOption();
expect(options.eq(1)).toEqualTrackedOption(10, 'ten');
expect(options.eq(2)).toEqualTrackedOption(20, 'twenty');
});
it('should update the selected option even if only the tracked property on the selected object changes (single)', function() {
createSelect({
'ng-model': 'selected',
'ng-options': 'item.label for item in arr track by item.id'
});
scope.$apply(function() {
scope.selected = {id: 10, label: 'ten'};
});
expect(element.val()).toEqual('10');
// Update the properties on the selected object, rather than replacing the whole object
scope.$apply(function() {
scope.selected.id = 20;
scope.selected.label = 'new twenty';
});
// The value of the select should change since the id property changed
expect(element.val()).toEqual('20');
// But the label of the selected option does not change
var option = element.find('option').eq(1);
expect(option.prop('selected')).toEqual(true);
expect(option.text()).toEqual('twenty'); // not 'new twenty'
});
it('should update the selected options even if only the tracked properties on the objects in the ' +
'selected collection change (multi)', function() {
createSelect({
'ng-model': 'selected',
'multiple': true,
'ng-options': 'item.label for item in arr track by item.id'
});
scope.$apply(function() {
scope.selected = [{id: 10, label: 'ten'}];
});
expect(element.val()).toEqual(['10']);
// Update the tracked property on the object in the selected array, rather than replacing the whole object
scope.$apply(function() {
scope.selected[0].id = 20;
});
// The value of the select should change since the id property changed
expect(element.val()).toEqual(['20']);
// But the label of the selected option does not change
var option = element.find('option').eq(1);
expect(option.prop('selected')).toEqual(true);
expect(option.text()).toEqual('twenty'); // not 'new twenty'
});
it('should prevent changes to the selected object from modifying the options objects (single)', function() {
createSelect({
'ng-model': 'selected',
'ng-options': 'item.label for item in arr track by item.id'
});
element.val('10');
browserTrigger(element, 'change');
expect(scope.selected).toEqual(scope.arr[0]);
scope.$apply(function() {
scope.selected.id = 20;
});
expect(scope.selected).not.toEqual(scope.arr[0]);
expect(element.val()).toEqual('20');
expect(scope.arr).toEqual([{id: 10, label: 'ten'}, {id:20, label: 'twenty'}]);
});
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'
});
scope.$apply(function() {
scope.selected = scope.arr[0];
});
expect(element.val()).toBe('10');
scope.$apply(function() {
scope.arr[0] = {id: 10, label: 'new ten'};
});
expect(element.val()).toBe('10');
element.children()[1].selected = 1;
browserTrigger(element, 'change');
expect(scope.selected).toEqual(scope.arr[1]);
});
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'
});
scope.$apply(function() {
scope.selected = scope.arr;
});
expect(element.val()).toEqual(['10','20']);
scope.$apply(function() {
scope.arr[0] = {id: 10, label: 'new ten'};
});
expect(element.val()).toEqual(['10','20']);
element.children()[0].selected = false;
browserTrigger(element, 'change');
expect(scope.selected).toEqual([scope.arr[1]]);
});
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'
});
scope.$apply(function() {
scope.selected = scope.obj['1'];
});
expect(element.val()).toBe('10');
scope.$apply(function() {
scope.obj['1'] = {score: 10, label: 'ten'};
});
expect(element.val()).toBe('10');
setSelectValue(element, 1);
expect(scope.selected).toEqual(scope.obj['2']);
});
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']);
element.children()[1].selected = 'selected';
browserTrigger(element, 'change');
expect(scope.selected).toEqual([scope.obj['1'], scope.obj['2']]);
});
it('should prevent infinite digest if track by expression is stable', function() {
scope.makeOptions = function() {
var options = [];
for (var i = 0; i < 5; i++) {
options.push({ label: 'Value = ' + i, value: i });
}
return options;
};
scope.selected = { label: 'Value = 1', value: 1 };
expect(function() {
createSelect({
'ng-model': 'selected',
'ng-options': 'item.label for item in makeOptions() track by item.value'
});
}).not.toThrow();
});
it('should re-render if the tracked property of the model is changed when using trackBy', function() {
createSelect({
'ng-model': 'selected',
'ng-options': 'item for item in arr track by item.id'
});
scope.$apply(function() {
scope.selected = {id: 10, label: 'ten'};
});
spyOn(element.controller('ngModel'), '$render');
scope.$apply(function() {
scope.arr[0].id = 20;
});
// update render due to equality watch
expect(element.controller('ngModel').$render).toHaveBeenCalled();
});
it('should not set view value again if the tracked property of the model has not changed when using trackBy', function() {
createSelect({
'ng-model': 'selected',
'ng-options': 'item for item in arr track by item.id'
});
scope.$apply(function() {
scope.selected = {id: 10, label: 'ten'};
});
spyOn(element.controller('ngModel'), '$setViewValue');
scope.$apply(function() {
scope.arr[0] = {id: 10, label: 'ten'};
});
expect(element.controller('ngModel').$setViewValue).not.toHaveBeenCalled();
});
it('should not re-render if a property of the model is changed when not using trackBy', function() {
createSelect({
'ng-model': 'selected',
'ng-options': 'item for item in arr'
});
scope.$apply(function() {
scope.selected = scope.arr[0];
});
spyOn(element.controller('ngModel'), '$render');
scope.$apply(function() {
scope.selected.label = 'changed';
});
// no render update as no equality watch
expect(element.controller('ngModel').$render).not.toHaveBeenCalled();
});
it('should handle options containing circular references (single)', function() {
scope.arr[0].ref = scope.arr[0];
createSelect({
'ng-model': 'selected',
'ng-options': 'item for item in arr track by item.id'
});
expect(function() {
scope.$apply(function() {
scope.selected = scope.arr[0];
});
}).not.toThrow();
});
it('should handle options containing circular references (multiple)', function() {
scope.arr[0].ref = scope.arr[0];
createSelect({
'ng-model': 'selected',
'multiple': true,
'ng-options': 'item for item in arr track by item.id'
});
expect(function() {
scope.$apply(function() {
scope.selected = [scope.arr[0]];
});
scope.$apply(function() {
scope.selected.push(scope.arr[1]);
});
}).not.toThrow();
});
it('should remove the "selected" attribute when the model changes', function() {
createSelect({
'ng-model': 'selected',
'ng-options': 'item.label for item in arr track by item.id'
});
var options = element.find('option');
browserTrigger(options[2], 'click');
expect(scope.selected).toEqual(scope.arr[1]);
scope.selected = {};
scope.$digest();
expect(options[0]).toBeMarkedAsSelected();
expect(options[2]).not.toBeMarkedAsSelected();
expect(options[2]).not.toBeMarkedAsSelected();
});
});
/**
* 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'}}};
});
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
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.val('10');
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()).toBe('10');
expect(scope.selected.id).toBe(10);
});
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)'
});
// 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);
});
});
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).toEqualSelectValue(scope.selected);
scope.$apply(function() {
scope.selected = scope.values[1];
});
expect(element).toEqualSelectValue(scope.selected);
});
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: 0},
{name: 'C', group: 'first'},
{name: 'D', group: 'second'},
{name: 'E', group: 0},
{name: 'F', group: 'first'},
{name: 'G', group: 'second'}];
scope.selected = scope.values[3];
});
expect(element).toEqualSelectValue(scope.selected);
var optgroups = element.find('optgroup');
expect(optgroups.length).toBe(3);
var zero = optgroups.eq(0);
var b = zero.find('option').eq(0);
var e = zero.find('option').eq(1);
expect(zero.attr('label')).toEqual('0');
expect(b.text()).toEqual('B');
expect(e.text()).toEqual('E');
var first = optgroups.eq(1);
var c = first.find('option').eq(0);
var f = first.find('option').eq(1);
expect(first.attr('label')).toEqual('first');
expect(c.text()).toEqual('C');
expect(f.text()).toEqual('F');
var second = optgroups.eq(2);
var d = second.find('option').eq(0);
var g = second.find('option').eq(1);
expect(second.attr('label')).toEqual('second');
expect(d.text()).toEqual('D');
expect(g.text()).toEqual('G');
scope.$apply(function() {
scope.selected = scope.values[0];
});
expect(element).toEqualSelectValue(scope.selected);
});
it('should group when the options are available on compile time', function() {
scope.values = [{name: 'C', group: 'first'},
{name: 'D', group: 'second'},
{name: 'F', group: 'first'},
{name: 'G', group: 'second'}];
scope.selected = scope.values[3];
createSelect({
'ng-model': 'selected',
'ng-options': 'item as item.name group by item.group for item in values'
});
expect(element).toEqualSelectValue(scope.selected);
var optgroups = element.find('optgroup');
expect(optgroups.length).toBe(2);
var first = optgroups.eq(0);
var c = first.find('option').eq(0);
var f = first.find('option').eq(1);
expect(first.attr('label')).toEqual('first');
expect(c.text()).toEqual('C');
expect(f.text()).toEqual('F');
var second = optgroups.eq(1);
var d = second.find('option').eq(0);
var g = second.find('option').eq(1);
expect(second.attr('label')).toEqual('second');
expect(d.text()).toEqual('D');
expect(g.text()).toEqual('G');
scope.$apply(function() {
scope.selected = scope.values[0];
});
expect(element).toEqualSelectValue(scope.selected);
});
it('should group when the options are updated', function() {
var optgroups, one, two, three, alpha, beta, gamma, delta, epsilon;
createSelect({
'ng-model': 'selected',
'ng-options': 'i.name group by i.cls for i in list'
});
scope.list = [
{cls: 'one', name: 'Alpha'},
{cls: 'one', name: 'Beta'},
{cls: 'two', name: 'Gamma'}
];
scope.$digest();
optgroups = element.find('optgroup');
expect(optgroups.length).toBe(2);
one = optgroups.eq(0);
expect(one.children('option').length).toBe(2);
alpha = one.find('option').eq(0);
beta = one.find('option').eq(1);
expect(one.attr('label')).toEqual('one');
expect(alpha.text()).toEqual('Alpha');
expect(beta.text()).toEqual('Beta');
two = optgroups.eq(1);
expect(two.children('option').length).toBe(1);
gamma = two.find('option').eq(0);
expect(two.attr('label')).toEqual('two');
expect(gamma.text()).toEqual('Gamma');
// Remove item from first group, add item to second group, add new group
scope.list.shift();
scope.list.push(
{cls: 'two', name: 'Delta'},
{cls: 'three', name: 'Epsilon'}
);
scope.$digest();
optgroups = element.find('optgroup');
expect(optgroups.length).toBe(3);
// Group with removed item
one = optgroups.eq(0);
expect(one.children('option').length).toBe(1);
beta = one.find('option').eq(0);
expect(one.attr('label')).toEqual('one');
expect(beta.text()).toEqual('Beta');
// Group with new item
two = optgroups.eq(1);
expect(two.children('option').length).toBe(2);
gamma = two.find('option').eq(0);
expect(two.attr('label')).toEqual('two');
expect(gamma.text()).toEqual('Gamma');
delta = two.find('option').eq(1);
expect(two.attr('label')).toEqual('two');
expect(delta.text()).toEqual('Delta');
// New group
three = optgroups.eq(2);
expect(three.children('option').length).toBe(1);
epsilon = three.find('option').eq(0);
expect(three.attr('label')).toEqual('three');
expect(epsilon.text()).toEqual('Epsilon');
});
it('should place non-grouped items in the list where they appear', 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'},
{name: 'E', group: 'first'},
{name: 'F'},
{name: 'G'},
{name: 'H', group: 'second'}];
scope.selected = scope.values[0];
});
var children = element.children();
expect(children.length).toEqual(6);
expect(nodeName_(children[0])).toEqual('option');
expect(nodeName_(children[1])).toEqual('optgroup');
expect(nodeName_(children[2])).toEqual('optgroup');
expect(nodeName_(children[3])).toEqual('option');
expect(nodeName_(children[4])).toEqual('option');
expect(nodeName_(children[5])).toEqual('option');
});
it('should group if the group has a falsy value (except undefined)', 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: ''},
{name: 'C', group: null},
{name: 'D', group: false},
{name: 'E', group: 0}];
scope.selected = scope.values[0];
});
var optgroups = element.find('optgroup');
var options = element.find('option');
expect(optgroups.length).toEqual(4);
expect(options.length).toEqual(5);
expect(optgroups[0].label).toBe('');
expect(optgroups[1].label).toBe('null');
expect(optgroups[2].label).toBe('false');
expect(optgroups[3].label).toBe('0');
expect(options[0].textContent).toBe('A');
expect(options[0].parentNode).toBe(element[0]);
expect(options[1].textContent).toBe('B');
expect(options[1].parentNode).toBe(optgroups[0]);
expect(options[2].textContent).toBe('C');
expect(options[2].parentNode).toBe(optgroups[1]);
expect(options[3].textContent).toBe('D');
expect(options[3].parentNode).toBe(optgroups[2]);
expect(options[4].textContent).toBe('E');
expect(options[4].parentNode).toBe(optgroups[3]);
});
it('should not duplicate a group with a falsy value when the options are updated', function() {
scope.$apply(function() {
scope.values = [{value: 'A', group: ''},
{value: 'B', group: 'First'}];
scope.selected = scope.values[0];
});
createSelect({
'ng-model': 'selected',
'ng-options': 'item.value group by item.group for item in values'
});
scope.$apply(function() {
scope.values.push({value: 'C', group: false});
});
var optgroups = element.find('optgroup');
var options = element.find('option');
expect(optgroups.length).toEqual(3);
expect(options.length).toEqual(3);
expect(optgroups[0].label).toBe('');
expect(optgroups[1].label).toBe('First');
expect(optgroups[2].label).toBe('false');
expect(options[0].textContent).toBe('A');
expect(options[0].parentNode).toBe(optgroups[0]);
expect(options[1].textContent).toBe('B');
expect(options[1].parentNode).toBe(optgroups[1]);
expect(options[2].textContent).toBe('C');
expect(options[2].parentNode).toBe(optgroups[2]);
});
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 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);
scope.$apply(function() {
scope.selected = scope.values[1].id;
});
expect(element).toEqualSelectValue(scope.selected);
});
it('should update options in the DOM', function() {
compile(
'<select ng-model="selected" ng-options="item.id as item.name for item in values"></select>'
);
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(10, 'C');
expect(options.eq(1)).toEqualOption(20, 'B');
});
it('should update options in the DOM from object source', function() {
compile(
'<select ng-model="selected" ng-options="val.id as val.name for (key, val) in values"></select>'
);
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(10, 'C');
expect(options.eq(1)).toEqualOption(20, '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).toEqualSelectValue(scope.selected);
scope.$apply(function() {
scope.selected = 'blue';
});
expect(element).toEqualSelectValue(scope.selected);
});
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).toEqualSelectValue(scope.selected);
scope.$apply(function() {
scope.selected = '0000FF';
});
expect(element).toEqualSelectValue(scope.selected);
});
it('should bind to object disabled', function() {
scope.selected = 30;
scope.options = [
{ name: 'white', value: '#FFFFFF' },
{ name: 'one', value: 1, unavailable: true },
{ name: 'notTrue', value: false },
{ name: 'thirty', value: 30, unavailable: false }
];
createSelect({
'ng-options': 'o.value as o.name disable when o.unavailable for o in options',
'ng-model': 'selected'
});
var options = element.find('option');
expect(scope.options[1].unavailable).toEqual(true);
expect(options.eq(1).prop('disabled')).toEqual(true);
scope.$apply(function() {
scope.options[1].unavailable = false;
});
options = element.find('option');
expect(scope.options[1].unavailable).toEqual(false);
expect(options.eq(1).prop('disabled')).toEqual(false);
});
it('should insert the unknown 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).toEqualSelectValue(scope.selected);
expect(element.find('option').length).toEqual(1);
});
it('should select the provided empty 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).toEqualSelectValue(scope.selected);
expect(jqLite(element.find('option')[0]).val()).toEqual('');
expect(element.find('option').length).toEqual(2);
});
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).toEqualSelectValue(scope.selected);
expect(element.find('option').length).toEqual(2);
});
it('should not insert a blank option if one of the options maps to null', function() {
createSelect({
'ng-model': 'myColor',
'ng-options': 'color.shade as color.name for color in colors'
});
scope.$apply(function() {
scope.colors = [
{name:'nothing', shade:null},
{name:'red', shade:'dark'}
];
scope.myColor = null;
});
expect(element.find('option').length).toEqual(2);
expect(element.find('option').eq(0)).toEqualOption(null);
expect(element.val()).not.toEqualUnknownValue(null);
expect(element.find('option').eq(0)).not.toEqualUnknownOption(null);
});
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()).toEqualUnknownValue(scope.selected);
expect(element.find('option').eq(0)).toEqualUnknownOption(scope.selected);
scope.$apply(function() {
scope.selected = scope.values[0];
});
expect(element).toEqualSelectValue(scope.selected);
expect(element.find('option').length).toEqual(1);
});
it('should insert and select temporary unknown option when no options-model match, empty ' +
'option is present and model is defined', function() {
scope.selected = 'C';
scope.values = [{name: 'A'}, {name: 'B'}];
createSingleSelect(true);
expect(element).toEqualSelect(['?'], '', 'object:3', 'object:4');
scope.$apply('selected = values[1]');
expect(element).toEqualSelect('', 'object:3', ['object:4']);
});
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()).toEqualUnknownValue();
expect(element.find('option').eq(0)).toEqualUnknownOption();
browserTrigger(element.find('option').eq(1));
expect(element.find('option').length).toEqual(2);
expect(element).toEqualSelectValue(scope.selected);
expect(element.find('option').eq(0).prop('selected')).toBeTruthy();
});
it('should remove unknown option when empty option exists and model is undefined', function() {
scope.selected = 'C';
scope.values = [{name: 'A'}, {name: 'B'}];
createSingleSelect(true);
expect(element).toEqualSelect(['?'], '', 'object:3', 'object:4');
scope.selected = undefined;
scope.$digest();
expect(element).toEqualSelect([''], 'object:3', 'object:4');
});
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()).toEqualUnknownValue();
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).toEqualSelectValue(scope.selected);
expect(element.find('option').eq(0).attr('selected')).toEqual('selected');
// Here the selected value is changed and we change the selected attribute
scope.$apply(function() {
scope.selected = 20;
});
expect(element).toEqualSelectValue(scope.selected);
expect(element.find('option').eq(1).attr('selected')).toEqual('selected');
scope.$apply(function() {
scope.values.push({id: 30, name: 'C'});
});
expect(element).toEqualSelectValue(scope.selected);
expect(element.find('option').eq(1).attr('selected')).toEqual('selected');
// Here the ? option should reappear and have selected attribute
scope.$apply(function() {
scope.selected = undefined;
});
expect(element.val()).toEqualUnknownValue();
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).toEqualUnknownOption();
});
it('should update the model if the selected option is removed', function() {
scope.values = [{value: 0, label: 'zero'}, {value: 1, label: 'one'}];
scope.selected = 1;
createSelect({
'ng-model': 'selected',
'ng-options': 'option.value as option.label for option in values'
});
expect(element).toEqualSelectValue(1);
// Check after initial option update
scope.$apply(function() {
scope.values.pop();
});
expect(element.val()).toEqual('?');
expect(scope.selected).toEqual(null);
// Check after model change
scope.$apply(function() {
scope.selected = 0;
});
expect(element).toEqualSelectValue(0);
scope.$apply(function() {
scope.values.pop();
});
expect(element.val()).toEqual('?');
expect(scope.selected).toEqual(null);
});
it('should update the model if all the selected (multiple) options are removed', function() {
scope.values = [{value: 0, label: 'zero'}, {value: 1, label: 'one'}, {value: 2, label: 'two'}];
scope.selected = [1, 2];
createSelect({
'ng-model': 'selected',
'multiple': true,
'ng-options': 'option.value as option.label for option in values'
});
expect(element).toEqualSelectValue([1, 2], true);
// Check after initial option update
scope.$apply(function() {
scope.values.pop();
});
expect(element).toEqualSelectValue([1], true);
expect(scope.selected).toEqual([1]);
scope.$apply(function() {
scope.values.pop();
});
expect(element).toEqualSelectValue([], true);
expect(scope.selected).toEqual([]);
// Check after model change
scope.$apply(function() {
scope.selected = [0];
});
expect(element).toEqualSelectValue([0], true);
scope.$apply(function() {
scope.values.pop();
});
expect(element).toEqualSelectValue([], true);
expect(scope.selected).toEqual([]);
});
});
describe('empty option', function() {
it('should be compiled as template, be watched and updated', function() {
var option;
createSingleSelect('<option value="">blank is {{blankVal}}</option>');
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('<option value="" ng-bind-template="blank is {{blankVal}}"></option>');
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 binding via ngBind attribute', function() {
var option;
createSingleSelect('<option value="" ng-bind="blankVal"></option>');
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 ignored when it has no value attribute', function() {
// The option value is set to the textContent if there's no value attribute,
// so in that case it doesn't count as a blank option
createSingleSelect('<option>--select--</option>');
scope.$apply(function() {
scope.values = [{name: 'A'}, {name: 'B'}, {name: 'C'}];
});
var options = element.find('option');
expect(options.eq(0)).toEqualUnknownOption();
expect(options.eq(1)).toEqualOption(scope.values[0], 'A');
expect(options.eq(2)).toEqualOption(scope.values[1], 'B');
expect(options.eq(3)).toEqualOption(scope.values[2], 'C');
});
it('should be rendered with the attributes preserved', function() {
var option;
createSingleSelect('<option value="" class="coyote" id="road-runner" ' +
'custom-attr="custom-attr">{{blankVal}}</option>');
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);
});
it('should be selectable if select is multiple', function() {
createMultiSelect(true);
// select the empty option
setSelectValue(element, 0);
// ensure selection and correct binding
expect(element[0].selectedIndex).toEqual(0);
expect(scope.selected).toEqual([]);
});
it('should be possible to use ngIf in the blank option', function() {
var option;
createSingleSelect('<option ng-if="isBlank" value="">blank</option>');
scope.$apply(function() {
scope.values = [{name: 'A'}];
scope.isBlank = true;
});
expect(element).toEqualSelect([''], 'object:4');
scope.$apply('isBlank = false');
expect(element).toEqualSelect(['?'], 'object:4');
scope.$apply('isBlank = true');
expect(element).toEqualSelect([''], 'object:4');
});
it('should be possible to use ngIf in the blank option when values are available upon linking',
function() {
var options;
scope.values = [{name: 'A'}];
createSingleSelect('<option ng-if="isBlank" value="">blank</option>');
scope.$apply('isBlank = true');
options = element.find('option');
expect(options.length).toBe(2);
expect(options.eq(0).val()).toBe('');
expect(options.eq(0).text()).toBe('blank');
scope.$apply('isBlank = false');
expect(element).toEqualSelect(['?'], 'object:3');
}
);
it('should select the correct option after linking when the ngIf expression is initially falsy', function() {
scope.values = [
{name:'black'},
{name:'white'},
{name:'red'}
];
scope.selected = scope.values[2];
expect(function() {
createSingleSelect('<option ng-if="isBlank" value="">blank</option>');
scope.$apply();
}).not.toThrow();
expect(element.find('option')[2]).toBeMarkedAsSelected();
expect(linkLog).toEqual(['linkNgOptions']);
});
it('should add / remove the "selected" attribute on empty option which has an initially falsy ngIf expression', function() {
scope.values = [
{name:'black'},
{name:'white'},
{name:'red'}
];
scope.selected = scope.values[2];
createSingleSelect('<option ng-if="isBlank" value="">blank</option>');
scope.$apply();
expect(element.find('option')[2]).toBeMarkedAsSelected();
scope.$apply('isBlank = true');
expect(element.find('option')[0].value).toBe('');
expect(element.find('option')[0]).not.toBeMarkedAsSelected();
scope.$apply('selected = null');
expect(element.find('option')[0].value).toBe('');
expect(element.find('option')[0]).toBeMarkedAsSelected();
scope.selected = scope.values[1];
scope.$apply();
expect(element.find('option')[0].value).toBe('');
expect(element.find('option')[0]).not.toBeMarkedAsSelected();
expect(element.find('option')[2]).toBeMarkedAsSelected();
});
it('should add / remove the "selected" attribute on empty option which has an initially truthy ngIf expression when no option is selected', function() {
scope.values = [
{name:'black'},
{name:'white'},
{name:'red'}
];
scope.isBlank = true;
createSingleSelect('<option ng-if="isBlank" value="">blank</option>');
scope.$apply();
expect(element.find('option')[0].value).toBe('');
expect(element.find('option')[0]).toBeMarkedAsSelected();
scope.selected = scope.values[2];
scope.$apply();
expect(element.find('option')[0]).not.toBeMarkedAsSelected();
expect(element.find('option')[3]).toBeMarkedAsSelected();
});
it('should add the "selected" attribute on empty option which has an initially falsy ngIf expression when no option is selected', function() {
scope.values = [
{name:'black'},
{name:'white'},
{name:'red'}
];
createSingleSelect('<option ng-if="isBlank" value="">blank</option>');
scope.$apply();
expect(element.find('option')[0]).not.toBeMarkedAsSelected();
scope.isBlank = true;
scope.$apply();
expect(element.find('option')[0].value).toBe('');
expect(element.find('option')[0]).toBeMarkedAsSelected();
expect(element.find('option')[1]).not.toBeMarkedAsSelected();
});
it('should not throw when a directive compiles the blank option before ngOptions is linked', function() {
expect(function() {
createSelect({
'o-compile-contents': '',
'name': 'select',
'ng-model': 'value',
'ng-options': 'item for item in items'
}, true);
}).not.toThrow();
expect(linkLog).toEqual(['linkCompileContents', 'linkNgOptions']);
});
it('should not throw with a directive that replaces', inject(function($templateCache, $httpBackend) {
$templateCache.put('select_template.html', '<select ng-options="option as option for option in selectable_options"> <option value="">This is a test</option> </select>');
scope.options = ['a', 'b', 'c', 'd'];
expect(function() {
element = $compile('<custom-select ng-model="value" options="options"></custom-select>')(scope);
scope.$digest();
}).not.toThrow();
dealoc(element);
}));
});
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 = '<select ng-model="someModel" ng-options="c for c in choices">' +
'<option value="" disabled>Choose One</option>' +
'</select>';
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 = '<select ng-model="someModel" ng-options="c for c in choices" required>' +
'<option value="" disabled>Choose One</option>' +
'</select>';
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);
});
// Support: Safari 9+
// This test relies defining a getter/setter `selected` property on either `<option>` elements
// or their prototype. Some browsers (including Safari 9) are very flakey when the
// getter/setter is not defined on the prototype (probably due to some bug). On Safari 9, the
// getter/setter that is already defined on the `<option>` element's prototype is not
// configurable, so we can't overwrite it with our spy.
if (!/\b(9|\d{2})(?:\.\d+)+[\s\S]*safari/i.test(window.navigator.userAgent)) {
it('should not re-set the `selected` property if it already has the correct value', function() {
scope.values = [{name: 'A'}, {name: 'B'}];
createMultiSelect();
var options = element.find('option');
var optionsSetSelected = [];
var _selected = [];
// Set up spies
var optionProto = Object.getPrototypeOf(options[0]);
var originalSelectedDescriptor = isFunction(Object.getOwnPropertyDescriptor) &&
Object.getOwnPropertyDescriptor(optionProto, 'selected');
var addSpiesOnProto = originalSelectedDescriptor && originalSelectedDescriptor.configurable;
forEach(options, function(option, i) {
var setSelected = function(value) { _selected[i] = value; };
optionsSetSelected[i] = jasmine.createSpy('optionSetSelected' + i).and.callFake(setSelected);
setSelected(option.selected);
});
if (!addSpiesOnProto) {
forEach(options, function(option, i) {
Object.defineProperty(option, 'selected', {
get: function() { return _selected[i]; },
set: optionsSetSelected[i]
});
});
} else {
// Support: Firefox 54+
// We cannot use the above (simpler) method on all browsers because of Firefox 54+, which
// is very flaky when the getter/setter property is defined on the element itself and not
// the prototype. (Possibly the result of some (buggy?) optimization.)
var getSelected = function(index) { return _selected[index]; };
var setSelected = function(index, value) { optionsSetSelected[index](value); };
var getSelectedOriginal = function(option) {
return originalSelectedDescriptor.get.call(option);
};
var setSelectedOriginal = function(option, value) {
originalSelectedDescriptor.set.call(option, value);
};
var getIndexAndCall = function(option, foundFn, notFoundFn, value) {
for (var i = 0, ii = options.length; i < ii; ++i) {
if (options[i] === option) return foundFn(i, value);
}
return notFoundFn(option, value);
};
Object.defineProperty(optionProto, 'selected', {
get: function() {
return getIndexAndCall(this, getSelected, getSelectedOriginal);
},
set: function(value) {
return getIndexAndCall(this, setSelected, setSelectedOriginal, value);
}
});
}
// Select `optionA`
scope.$apply('selected = [values[0]]');
expect(optionsSetSelected[0]).toHaveBeenCalledOnceWith(true);
expect(optionsSetSelected[1]).not.toHaveBeenCalled();
expect(options[0].selected).toBe(true);
expect(options[1].selected).toBe(false);
optionsSetSelected[0].calls.reset();
optionsSetSelected[1].calls.reset();
// Select `optionB` (`optionA` remains selected)
scope.$apply('selected.push(values[1])');
expect(optionsSetSelected[0]).not.toHaveBeenCalled();
expect(optionsSetSelected[1]).toHaveBeenCalledOnceWith(true);
expect(options[0].selected).toBe(true);
expect(options[1].selected).toBe(true);
optionsSetSelected[0].calls.reset();
optionsSetSelected[1].calls.reset();
// Unselect `optionA` (`optionB` remains selected)
scope.$apply('selected.shift()');
expect(optionsSetSelected[0]).toHaveBeenCalledOnceWith(false);
expect(optionsSetSelected[1]).not.toHaveBeenCalled();
expect(options[0].selected).toBe(false);
expect(options[1].selected).toBe(true);
optionsSetSelected[0].calls.reset();
optionsSetSelected[1].calls.reset();
// Reselect `optionA` (`optionB` remains selected)
scope.$apply('selected.push(values[0])');
expect(optionsSetSelected[0]).toHaveBeenCalledOnceWith(true);
expect(optionsSetSelected[1]).not.toHaveBeenCalled();
expect(options[0].selected).toBe(true);
expect(options[1].selected).toBe(true);
optionsSetSelected[0].calls.reset();
optionsSetSelected[1].calls.reset();
// Unselect `optionB` (`optionA` remains selected)
scope.$apply('selected.shift()');
expect(optionsSetSelected[0]).not.toHaveBeenCalled();
expect(optionsSetSelected[1]).toHaveBeenCalledOnceWith(false);
expect(options[0].selected).toBe(true);
expect(options[1].selected).toBe(false);
optionsSetSelected[0].calls.reset();
optionsSetSelected[1].calls.reset();
// Unselect `optionA`
scope.$apply('selected.length = 0');
expect(optionsSetSelected[0]).toHaveBeenCalledOnceWith(false);
expect(optionsSetSelected[1]).not.toHaveBeenCalled();
expect(options[0].selected).toBe(false);
expect(options[1].selected).toBe(false);
optionsSetSelected[0].calls.reset();
optionsSetSelected[1].calls.reset();
// Support: Firefox 54+
// Restore `originalSelectedDescriptor`
if (addSpiesOnProto) {
Object.defineProperty(optionProto, 'selected', originalSelectedDescriptor);
}
});
}
if (window.MutationObserver) {
//IE9 and IE10 do not support MutationObserver
//Since the feature is only needed for a test, it's okay to skip these browsers
it('should render the initial options only one time', function() {
scope.value = ['black'];
scope.values = ['black', 'white', 'red'];
// observe-child-list adds a MutationObserver that we will read out after ngOptions
// has been compiled
createSelect({
'ng-model':'selected',
'ng-options':'value.name for value in values',
'multiple': 'true',
'observe-child-list': ''
});
var optionEls = element[0].querySelectorAll('option');
var records = childListMutationObserver.takeRecords();
expect(records.length).toBe(1);
expect(records[0].addedNodes).toEqual(optionEls);
});
}
});
describe('required state', function() {
it('should set the error if the empty option is selected', function() {
createSelect({
'ng-model': 'selection',
'ng-options': 'item for item in values',
'required': ''
}, true);
scope.$apply(function() {
scope.values = ['a', 'b'];
scope.selection = scope.values[0];
});
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');
expect(options[0]).toBeMarkedAsSelected();
expect(element).toBeInvalid();
expect(ngModelCtrl.$error.required).toBeTruthy();
});
it('should validate with empty option and bound 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;
});
var options = element.find('option');
browserTrigger(options[0], 'click');
expect(element).toBeValid();
scope.$apply('required = true');
expect(element).toBeInvalid();
scope.$apply('value = values[0]');
expect(element).toBeValid();
browserTrigger(options[0], 'click');
expect(element).toBeInvalid();
scope.$apply('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 NOT set the error if the empty option is present but required attribute is not',
function() {
scope.$apply(function() {
scope.values = ['a', 'b'];
});
createSingleSelect();
expect(element).toBeValid();
expect(element).toBePristine();
expect(ngModelCtrl.$error.required).toBeFalsy();
}
);
it('should NOT set the error if the unknown option is selected', function() {
createSelect({
'ng-model': 'selection',
'ng-options': 'item for item in values',
'required': ''
});
scope.$apply(function() {
scope.values = ['a', 'b'];
scope.selection = 'a';
});
expect(element).toBeValid();
expect(ngModelCtrl.$error.required).toBeFalsy();
scope.$apply('selection = "c"');
expect(element).toEqualSelect(['?'], 'string:a', 'string:b');
expect(element).toBeValid();
expect(ngModelCtrl.$error.required).toBeFalsy();
});
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);
});
it('should validate after option list was updated', function() {
createSelect({
'ng-model': 'selection',
'ng-options': 'item for item in values',
'required': ''
}, true);
scope.$apply(function() {
scope.values = ['A', 'B'];
scope.selection = scope.values[0];
});
expect(element).toEqualSelect('', ['string:A'], 'string:B');
expect(element).toBeValid();
expect(ngModelCtrl.$error.required).toBeFalsy();
scope.$apply(function() {
scope.values = ['C', 'D'];
});
expect(element).toEqualSelect([''], 'string:C', 'string:D');
expect(element).toBeInvalid();
expect(ngModelCtrl.$error.required).toBeTruthy();
// ngModel sets undefined for invalid values
expect(scope.selection).toBeUndefined();
});
});
describe('required and empty option', function() {
it('should select the empty option after compilation', function() {
createSelect({
'name': 'select',
'ng-model': 'value',
'ng-options': 'item for item in [\'first\', \'second\', \'third\']',
'required': 'required'
}, true);
expect(element.val()).toBe('');
var emptyOption = element.find('option').eq(0);
expect(emptyOption.prop('selected')).toBe(true);
expect(emptyOption.val()).toBe('');
});
});
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');
}));
it('should not set $dirty with select-multiple after compilation', function() {
scope.values = ['a', 'b'];
scope.selected = ['b'];
createSelect({
'ng-model':'selected',
'multiple':true,
'ng-options':'value for value in values',
'name': 'select'
});
expect(element.find('option')[1].selected).toBe(true);
expect(scope.form.select.$pristine).toBe(true);
});
});
describe('selectCtrl api', function() {
it('should reflect the status of empty and unknown option', function() {
createSingleSelect('<option ng-if="isBlank" value="">blank</option>');
var selectCtrl = element.controller('select');
scope.$apply(function() {
scope.values = [{name: 'A'}, {name: 'B'}];
scope.isBlank = true;
});
expect(element).toEqualSelect([''], 'object:4', 'object:5');
expect(selectCtrl.$hasEmptyOption()).toBe(true);
expect(selectCtrl.$isEmptyOptionSelected()).toBe(true);
expect(selectCtrl.$isUnknownOptionSelected()).toBe(false);
// empty -> selection
scope.$apply(function() {
scope.selected = scope.values[0];
});
expect(element).toEqualSelect('', ['object:4'], 'object:5');
expect(selectCtrl.$hasEmptyOption()).toBe(true);
expect(selectCtrl.$isEmptyOptionSelected()).toBe(false);
expect(selectCtrl.$isUnknownOptionSelected()).toBe(false);
// remove empty
scope.$apply('isBlank = false');
expect(element).toEqualSelect(['object:4'], 'object:5');
expect(selectCtrl.$hasEmptyOption()).toBe(false);
expect(selectCtrl.$isEmptyOptionSelected()).toBe(false);
expect(selectCtrl.$isUnknownOptionSelected()).toBe(false);
// selection -> unknown
scope.$apply('selected = "unmatched"');
expect(element).toEqualSelect(['?'], 'object:4', 'object:5');
expect(selectCtrl.$hasEmptyOption()).toBe(false);
expect(selectCtrl.$isEmptyOptionSelected()).toBe(false);
expect(selectCtrl.$isUnknownOptionSelected()).toBe(true);
// add empty
scope.$apply('isBlank = true');
expect(element).toEqualSelect(['?'], '', 'object:4', 'object:5');
expect(selectCtrl.$hasEmptyOption()).toBe(true);
expect(selectCtrl.$isEmptyOptionSelected()).toBe(false);
expect(selectCtrl.$isUnknownOptionSelected()).toBe(true);
// unknown -> empty
scope.$apply(function() {
scope.selected = null;
});
expect(element).toEqualSelect([''], 'object:4', 'object:5');
expect(selectCtrl.$hasEmptyOption()).toBe(true);
expect(selectCtrl.$isEmptyOptionSelected()).toBe(true);
expect(selectCtrl.$isUnknownOptionSelected()).toBe(false);
// empty -> unknown
scope.$apply('selected = "unmatched"');
expect(element).toEqualSelect(['?'], '', 'object:4', 'object:5');
expect(selectCtrl.$hasEmptyOption()).toBe(true);
expect(selectCtrl.$isEmptyOptionSelected()).toBe(false);
expect(selectCtrl.$isUnknownOptionSelected()).toBe(true);
// unknown -> selection
scope.$apply(function() {
scope.selected = scope.values[1];
});
expect(element).toEqualSelect('', 'object:4', ['object:5']);
expect(selectCtrl.$hasEmptyOption()).toBe(true);
expect(selectCtrl.$isEmptyOptionSelected()).toBe(false);
expect(selectCtrl.$isUnknownOptionSelected()).toBe(false);
// selection -> empty
scope.$apply('selected = null');
expect(element).toEqualSelect([''], 'object:4', 'object:5');
expect(selectCtrl.$hasEmptyOption()).toBe(true);
expect(selectCtrl.$isEmptyOptionSelected()).toBe(true);
expect(selectCtrl.$isUnknownOptionSelected()).toBe(false);
});
});
});