angular-material-npfixed
Version:
The Angular Material project is an implementation of Material Design in Angular.js. This project provides a set of reusable, well-tested, and accessible Material Design UI components. Angular Material is supported internally at Google by the Angular.js, M
1,354 lines (1,060 loc) • 52.9 kB
JavaScript
describe('<md-select>', function() {
var attachedElements = [];
var body, $document, $rootScope, $compile, $timeout, $material;
beforeEach(function() {
module('material.components.select', 'material.components.input', 'ngSanitize');
inject(function($injector) {
$document = $injector.get('$document');
$rootScope = $injector.get('$rootScope');
$compile = $injector.get('$compile');
$timeout = $injector.get('$timeout');
$material = $injector.get('$material');
body = $document[0].body;
});
});
afterEach(function() {
var body = $document[0].body;
var children = body.querySelectorAll('.md-select-menu-container');
for (var i = 0; i < children.length; i++) {
angular.element(children[i]).remove();
}
});
afterEach(function() {
attachedElements.forEach(function(element) {
var scope = element.scope();
scope && scope.$destroy();
element.remove();
});
attachedElements = [];
$document.find('md-select-menu').remove();
$document.find('md-backdrop').remove();
});
describe('basic functionality', function() {
it('should have `._md` class indicator', function() {
var element = setupSelect('ng-model="val"').find('md-select-menu');
expect(element.hasClass('_md')).toBe(true);
});
it('should preserve tabindex', function() {
var select = setupSelect('tabindex="2" ng-model="val"').find('md-select');
expect(select.attr('tabindex')).toBe('2');
});
it('should set a tabindex if the element does not have one', function() {
var select = setupSelect('ng-model="val"').find('md-select');
expect(select.attr('tabindex')).toBeDefined();
});
it('supports non-disabled state', function() {
var select = setupSelect('ng-model="val"').find('md-select');
expect(select.attr('aria-disabled')).toBe('false');
});
it('supports disabled state', function() {
var select = setupSelect('disabled ng-model="val"').find('md-select');
openSelect(select);
expectSelectClosed(select);
expect($document.find('md-select-menu').length).toBe(0);
expect(select.attr('aria-disabled')).toBe('true');
});
it('supports passing classes to the container', function() {
var select = setupSelect('ng-model="val" md-container-class="test"').find('md-select');
openSelect(select);
var container = $document[0].querySelector('.md-select-menu-container');
expect(container).toBeTruthy();
expect(container.classList.contains('test')).toBe(true);
});
it('supports passing classes to the container using `data-` attribute prefix', function() {
var select = setupSelect('ng-model="val" data-md-container-class="test"').find('md-select');
openSelect(select);
var container = $document[0].querySelector('.md-select-menu-container');
expect(container).toBeTruthy();
expect(container.classList.contains('test')).toBe(true);
});
it('supports passing classes to the container using `x-` attribute prefix', function() {
var select = setupSelect('ng-model="val" x-md-container-class="test"').find('md-select');
openSelect(select);
var container = $document[0].querySelector('.md-select-menu-container');
expect(container).toBeTruthy();
expect(container.classList.contains('test')).toBe(true);
});
it('sets aria-owns between the select and the container', function() {
var select = setupSelect('ng-model="val"').find('md-select');
var ownsId = select.attr('aria-owns');
expect(ownsId).toBeTruthy();
var containerId = select[0].querySelector('.md-select-menu-container').getAttribute('id');
expect(ownsId).toBe(containerId);
});
it('calls md-on-close when the select menu closes', function() {
var called = false;
$rootScope.onClose = function() {
called = true;
};
var select = setupSelect('ng-model="val" md-on-close="onClose()"', [1, 2, 3]).find('md-select');
openSelect(select);
expectSelectOpen(select);
clickOption(select, 0);
$material.flushInterimElement();
expectSelectClosed(select);
expect(called).toBe(true);
});
it('closes on backdrop click', function() {
var select = setupSelect('ng-model="val"', [1, 2, 3]).find('md-select');
openSelect(select);
// Simulate click bubble from option to select menu handler
var backdrop = $document.find('md-backdrop');
expect(backdrop.length).toBe(1);
backdrop.triggerHandler('click');
$material.flushInterimElement();
backdrop = $document.find('md-backdrop');
expect(backdrop.length).toBe(0);
});
it('removes the menu container when the select is removed', function() {
var select = setupSelect('ng-model="val"', [1]).find('md-select');
openSelect(select);
select.remove();
expect($document.find('md-select-menu').length).toBe(0);
});
it('should not trigger ng-change without a change when using trackBy', function() {
var changed = false;
$rootScope.onChange = function() { changed = true; };
$rootScope.val = { id: 1, name: 'Bob' };
var opts = [ { id: 1, name: 'Bob' }, { id: 2, name: 'Alice' } ];
var select = setupSelect('ng-model="$root.val" ng-change="onChange()" ng-model-options="{trackBy: \'$value.id\'}"', opts);
expect(changed).toBe(false);
openSelect(select);
clickOption(select, 1);
$material.flushInterimElement();
expect($rootScope.val.id).toBe(2);
expect(changed).toBe(true);
});
it('should set touched only after closing', function() {
var form = $compile('<form name="myForm">' +
'<md-select name="select" ng-model="val">' +
'<md-option>1</md-option>' +
'</md-select>' +
'</form>')($rootScope);
var select = form.find('md-select');
openSelect(select);
expect($rootScope.myForm.select.$touched).toBe(false);
closeSelect();
expect($rootScope.myForm.select.$touched).toBe(true);
});
it('should remain untouched during opening', function() {
var form = $compile('<form name="myForm">' +
'<md-select name="select" ng-model="val">' +
'<md-option>1</md-option>' +
'</md-select>' +
'</form>')($rootScope);
var unwatch = $rootScope.$watch('myForm.select.$touched',
function(touched) {
expect(touched).toBe(false);
});
var select = form.find('md-select');
openSelect(select);
unwatch();
closeSelect();
expect($rootScope.myForm.select.$touched).toBe(true);
});
it('applies the md-input-focused class to the container when focused with the keyboard', function() {
var element = setupSelect('ng-model="val"');
var select = element.find('md-select');
select.triggerHandler('focus');
expect(element.hasClass('md-input-focused')).toBe(true);
select.triggerHandler('blur');
expect(element.hasClass('md-input-focused')).toBe(false);
});
it('restores focus to select when the menu is closed', function() {
var select = setupSelect('ng-model="val"').find('md-select');
openSelect(select);
$document[0].body.appendChild(select[0]);
var selectMenu = $document.find('md-select-menu');
pressKey(selectMenu, 27);
$material.flushInterimElement();
// FIXME- does not work with minified, jquery
//expect($document[0].activeElement).toBe(select[0]);
// Clean up the DOM after the test.
$document[0].body.removeChild(select[0]);
});
it('should remove the input-container focus state', function() {
$rootScope.val = 0;
var element = setupSelect('ng-model="val"', [1, 2, 3]);
var select = element.find('md-select');
var controller = element.controller('mdInputContainer');
$timeout.flush();
controller.setHasValue(true);
select.triggerHandler('focus');
expect(element.hasClass('md-input-focused')).toBe(true);
select.triggerHandler('blur');
expect(element.hasClass('md-input-focused')).toBe(false);
});
it('should remove the tabindex from a disabled element', function() {
var select = setupSelect('ng-model="val" disabled tabindex="1"').find('md-select');
expect(select.attr('tabindex')).toBeUndefined();
});
it('auto-infers a value when none specified', function() {
$rootScope.name = "Hannah";
var el = setupSelect('ng-model="name"', '<md-option>Tom</md-option>' +
'<md-option>Hannah</md-option>');
expect(selectedOptions(el).length).toBe(1);
});
it('errors for duplicate md-options, non-dynamic value', function() {
expect(function() {
setupSelect('ng-model="$root.model"', ['a', 'a']);
}).toThrow();
});
it('errors for duplicate md-options, ng-value', function() {
setupSelect('ng-model="$root.model"', '<md-option ng-value="foo">Hello</md-option>' +
'<md-option ng-value="bar">Goodbye</md-option>');
$rootScope.$apply('foo = "a"');
expect(function() {
$rootScope.$apply('bar = "a"');
}).toThrow();
});
it('watches the collection for changes', function() {
$rootScope.val = 1;
var select = setupSelect('ng-model="val"', [1, 2, 3]).find('md-select');
var label = select.find('md-select-value')[0];
expect(label.textContent).toBe('1');
$rootScope.val = 4;
$rootScope.$$values = [4, 5, 6];
$rootScope.$digest();
expect(label.textContent).toBe('4');
});
describe('when required', function() {
it('allows 0 as a valid default value', function() {
$rootScope.model = 0;
$rootScope.opts = [0, 1, 2];
$compile('<form name="testForm">' +
'<md-select ng-model="model" name="defaultSelect" required>' +
'<md-option ng-repeat="opt in opts" ng-value="opt"></md-option>' +
'</md-select></form>')($rootScope);
$rootScope.$digest();
$timeout.flush();
expect($rootScope.testForm.defaultSelect.$error).toEqual({});
});
});
});
describe('input container', function() {
it('should set has-value class on container for non-ng-model input', function() {
var el = setupSelect('ng-model="$root.model"', [1, 2, 3]);
var select = el.find('md-select');
openSelect(select);
clickOption(select, 0);
$material.flushInterimElement();
expect(el).toHaveClass('md-input-has-value');
});
it('should set has-value class on container for ng-model input', function() {
$rootScope.value = 'test';
var el = setupSelect('ng-model="$root.value"', ['test', 'no-test']);
expect(el).toHaveClass('md-input-has-value');
$rootScope.$apply('value = null');
expect(el).not.toHaveClass('md-input-has-value');
});
it('should add has-value class on container for option ng-value="undefined"', function() {
var el = setupSelect('ng-model="$root.value"',
'<md-option ng-value="undefined"></md-option><md-option ng-value="1">1</md-option>'
);
var select = el.find('md-select');
document.body.appendChild(el[0]);
openSelect(select);
$material.flushInterimElement();
clickOption(select, 0);
closeSelect();
$material.flushInterimElement();
expect(el).toHaveClass('md-input-has-value');
openSelect(select);
$material.flushInterimElement();
clickOption(select, 1);
closeSelect();
$material.flushInterimElement();
expect(el).toHaveClass('md-input-has-value');
el.remove();
});
[
'<md-option></md-option>',
'<md-option value></md-option>',
'<md-option value>None</md-option>',
'<md-option ng-value></md-option>',
'<md-option ng-value>None</md-option>',
'<md-option value=""></md-option>',
'<md-option ng-value=""></md-option>'
].forEach(function(template) {
it('should unset has-value class on container for empty value option (' + template + ')', function() {
var el = setupSelect('ng-model="$root.value"',
template + '<md-option ng-value="1">1</md-option>'
);
var select = el.find('md-select');
document.body.appendChild(el[0]);
openSelect(select);
$material.flushInterimElement();
clickOption(select, 1);
closeSelect();
$material.flushInterimElement();
expect(el).toHaveClass('md-input-has-value');
openSelect(select);
$material.flushInterimElement();
clickOption(select, 0);
closeSelect();
$material.flushInterimElement();
expect(el).not.toHaveClass('md-input-has-value');
el.remove();
});
});
it('should match label to given input id', function() {
var el = setupSelect('ng-model="$root.value" id="foo"');
expect(el.find('label').attr('for')).toBe('foo');
expect(el.find('md-select').attr('id')).toBe('foo');
});
it('should match label to automatic input id', function() {
var el = setupSelect('ng-model="$root.value"');
expect(el.find('md-select').attr('id')).toBeTruthy();
expect(el.find('label').attr('for')).toBe(el.find('md-select').attr('id'));
});
});
describe('label behavior', function() {
it('defaults to the placeholder text', function() {
var select = setupSelect('ng-model="someVal" placeholder="Hello world"', null, true).find('md-select');
var label = select.find('md-select-value');
expect(label.text()).toBe('Hello world');
expect(label.hasClass('md-select-placeholder')).toBe(true);
});
it('sets itself to the selected option\'s label', function() {
$rootScope.val = 2;
var select = $compile('<md-input-container>' +
'<label>Label</label>' +
'<md-select ng-model="val" placeholder="Hello World">' +
'<md-option value="1">One</md-option>' +
'<md-option value="2">Two</md-option>' +
'<md-option value="3">Three</md-option>' +
'</md-select>' +
'</md-input-container>')($rootScope).find('md-select');
var label = select.find('md-select-value');
var options = select.find('md-option');
$rootScope.$digest();
expect(label.text()).toBe('Two');
expect(label.hasClass('md-select-placeholder')).toBe(false);
// Ensure every md-option element does not have a checkbox prepended to it.
for (var i = 0; i < options.length; i++) {
var checkBoxContainer = options[i].querySelector('.md-container');
var checkBoxIcon = options[i].querySelector('.md-icon');
expect(checkBoxContainer).toBe(null);
expect(checkBoxIcon).toBe(null);
}
});
it('displays md-selected-text when specified', function() {
$rootScope.selectedText = 'Hello World';
var select = setupSelect('ng-model="someVal", md-selected-text="selectedText"', null, true).find('md-select');
var label = select.find('md-select-value');
expect(label.text()).toBe($rootScope.selectedText);
$rootScope.selectedText = 'Goodbye world';
// The label update function is not called until some user action occurs.
openSelect(select);
closeSelect(select);
$material.flushInterimElement();
expect(label.text()).toBe($rootScope.selectedText);
});
it('should sanitize md-selected-html', function() {
$rootScope.selectedText = '<b>Hello World</b><script>window.mdSelectXss="YES"</script>';
var select = setupSelect(
'ng-model="someVal", ' +
'md-selected-html="selectedText"', null, true).find('md-select');
var label = select.find('md-select-value');
expect(label.text()).toBe('Hello World');
// The label is loaded into a span that is the first child of the '<md-select-value>`.
expect(label[0].childNodes[0].innerHTML).toBe('<b>Hello World</b>');
expect(window.mdSelectXss).toBeUndefined();
});
it('should always treat md-selected-text as text, not html', function() {
$rootScope.selectedText = '<b>Hello World</b>';
var select = setupSelect(
'ng-model="someVal", ' +
'md-selected-text="selectedText"', null, true).find('md-select');
var label = select.find('md-select-value');
expect(label.text()).toBe('<b>Hello World</b>');
});
it('supports rendering multiple', function() {
$rootScope.val = [1, 3];
var select = $compile('<md-input-container>' +
'<label>Label</label>' +
'<md-select multiple ng-model="val" placeholder="Hello World">' +
'<md-option value="1">One</md-option>' +
'<md-option value="2">Two</md-option>' +
'<md-option value="3">Three</md-option>' +
'</md-select>' +
'</md-input-container>')($rootScope).find('md-select');
var label = select.find('md-select-value');
var options = select.find('md-option');
$rootScope.$digest();
$rootScope.$digest();
expect(label.text()).toBe('One, Three');
expect(label.hasClass('md-select-placeholder')).toBe(false);
// Ensure every md-option element has a checkbox prepended to it.
for (var i = 0; i < options.length; i++) {
var checkBoxContainer = options[i].querySelector('.md-container');
var checkBoxIcon = options[i].querySelector('.md-icon');
expect(checkBoxContainer).not.toBe(null);
expect(checkBoxIcon).not.toBe(null);
}
});
describe('md-select-header behavior', function() {
it('supports rendering a md-select-header', function() {
$rootScope.val = [1];
var select = $compile(
'<md-input-container>' +
' <label>Label</label>' +
' <md-select multiple ng-model="val" placeholder="Hello World">' +
' <md-select-header class="demo-select-header">' +
' <span>Hello World</span>' +
' </md-select-header>' +
' <md-optgroup label="stuff">' +
' <md-option value="1">One</md-option>' +
' <md-option value="2">Two</md-option>' +
' <md-option value="3">Three</md-option>' +
' </md-optgroup>' +
' </md-select>' +
'</md-input-container>')($rootScope);
var header = select.find('md-select-header');
var headerContent = header.find('span');
expect(headerContent.text()).toBe('Hello World');
});
it('does not render the label in md-optgroup if md-select-header is present', function() {
$rootScope.val = [1];
var select = $compile(
'<md-input-container>' +
' <label>Label</label>' +
' <md-select multiple ng-model="val" placeholder="Hello World">' +
' <md-select-header class="demo-select-header">' +
' <span>Hello World</span>' +
' </md-select-header>' +
' <md-optgroup label="stuff">' +
' <md-option value="1">One</md-option>' +
' <md-option value="2">Two</md-option>' +
' <md-option value="3">Three</md-option>' +
' </md-optgroup>' +
' </md-select>' +
'</md-input-container>')($rootScope);
var optgroupLabel = select[0].querySelector('.md-container-ignore');
expect(optgroupLabel).toBe(null);
});
});
it('does not allow keydown events to propagate from inside the md-select-menu', function() {
var scope = $rootScope.$new();
scope.val = [1];
var select = $compile(
'<md-input-container>' +
' <label>Label</label>' +
' <md-select multiple ng-model="val" placeholder="Hello World">' +
' <md-option value="1">One</md-option>' +
' <md-option value="2">Two</md-option>' +
' <md-option value="3">Three</md-option>' +
' </md-select>' +
'</md-input-container>')(scope);
var mdOption = select.find('md-option');
var selectMenu = select.find('md-select-menu');
var keydownEvent = {
type: 'keydown',
target: mdOption[0],
preventDefault: jasmine.createSpy(),
stopPropagation: jasmine.createSpy()
};
openSelect(select);
angular.element(selectMenu).triggerHandler(keydownEvent);
expect(keydownEvent.preventDefault).toHaveBeenCalled();
expect(keydownEvent.stopPropagation).toHaveBeenCalled();
scope.$destroy();
select.remove();
});
it('supports raw html', inject(function($sce) {
$rootScope.val = 0;
$rootScope.opts = [
{ id: 0, label: '<p>Hello World</p>' },
{ id: 1, label: 'Hello World' }
];
angular.forEach($rootScope.opts, function(opt) {
opt.label = $sce.trustAs('html', opt.label);
});
var select = $compile('<md-input-container>' +
'<label>Placeholder</label>' +
'<md-select ng-model="val" placeholder="Placeholder">' +
'<md-option ng-value="opt.id" ng-repeat="opt in opts" ng-bind-html="opt.label"></md-option>' +
'</md-select>' +
'</md-input-container>')($rootScope).find('md-select');
var label = select.find('md-select-value').children().eq(0);
$rootScope.$digest();
expect(label.text()).toBe('Hello World');
expect(label.html()).toBe('<p>Hello World</p>');
}));
});
describe('non-multiple', function() {
describe('model->view', function() {
it('renders initial model value', function() {
$rootScope.$apply('model = "b"');
var el = setupSelect('ng-model="$root.model"', ['a','b','c']);
expect(selectedOptions(el).length).toBe(1);
expect(el.find('md-option').eq(1).attr('selected')).toBe('selected');
});
it('renders nothing if no initial value is set', function() {
var el = setupSelect('ng-model="$root.model"', ['a','b','c']);
expect(selectedOptions(el).length).toBe(0);
});
it('supports circular references', function() {
var opts = [{ id: 1 }, { id: 2 }];
opts[0].refs = opts[1];
opts[1].refs = opts[0];
setupSelect('ng-model="$root.model"', opts, undefined, undefined, { renderValueAs: 'value.id' });
});
it('renders model change by selecting new and deselecting old', function() {
$rootScope.$apply('model = "b"');
var el = setupSelect('ng-model="$root.model"', ['a','b','c']);
expect(selectedOptions(el).length).toBe(1);
expect(el.find('md-option').eq(1).attr('selected')).toBe('selected');
$rootScope.$apply('model = "c"');
expect(el.find('md-option').eq(2).attr('selected')).toBe('selected');
expect(selectedOptions(el).length).toBe(1);
});
it('renders invalid model change by deselecting old and selecting nothing', function() {
$rootScope.$apply('model = "b"');
var el = setupSelect('ng-model="$root.model"', ['a','b','c']);
expect(selectedOptions(el).length).toBe(1);
expect(el.find('md-option').eq(1).attr('selected')).toBe('selected');
$rootScope.$apply('model = "d"');
expect(selectedOptions(el).length).toBe(0);
});
it('renders model change to undefined by deselecting old and selecting nothing', function() {
$rootScope.$apply('model = "b"');
var el = setupSelect('ng-model="$root.model"', ['a','b','c']);
expect(selectedOptions(el).length).toBe(1);
expect(el.find('md-option').eq(1).attr('selected')).toBe('selected');
$rootScope.$apply('model = undefined');
expect(selectedOptions(el).length).toBe(0);
});
it('uses track by if given to compare objects', function() {
$rootScope.$apply('model = {id:2}');
var el = setupSelect('ng-model="$root.model" ng-model-options="{trackBy: \'$value.id\'}"',
[{id:1}, {id:2}, {id:3}]);
expect(selectedOptions(el).length).toBe(1);
expect(el.find('md-option').eq(1).attr('selected')).toBe('selected');
$rootScope.$apply('model = {id: 3}');
expect(selectedOptions(el).length).toBe(1);
expect(el.find('md-option').eq(2).attr('selected')).toBe('selected');
});
it('uses uid by default to compare objects', function() {
var one = {}, two = {}, three = {};
$rootScope.model = two;
var el = setupSelect('ng-model="$root.model"', [one, two, three]);
expect(selectedOptions(el).length).toBe(1);
expect(el.find('md-option').eq(1).attr('selected')).toBe('selected');
$rootScope.$apply('model = {}');
expect(selectedOptions(el).length).toBe(0);
});
it('should keep the form pristine when model is predefined', function() {
$rootScope.model = 2;
$rootScope.opts = [1, 2, 3, 4];
$compile('<form name="testForm">' +
'<md-select ng-model="model" name="multiSelect">' +
'<md-option ng-repeat="opt in opts" ng-value="opt"></md-option>' +
'</md-select></form>')($rootScope);
$rootScope.$digest();
$timeout.flush();
expect($rootScope.testForm.$pristine).toBe(true);
});
it('should forward the model value to the hidden select', function() {
$rootScope.opts = [1, 2, 3, 4];
var select = $compile('<form>' +
'<md-select ng-model="model" name="testing-select">' +
'<md-option ng-repeat="opt in opts">{{ opt }}</md-option>' +
'</md-select></form>')($rootScope).find('select'); // not md-select
$rootScope.$digest();
$timeout.flush();
expect(select.val()).toBeFalsy();
$rootScope.$apply('model = 3');
expect(select.val()).toBe('3');
});
it('should forward the name attribute to the hidden select', function() {
var select = $compile('<form>' +
'<md-select ng-model="model" name="testing-select">' +
'</md-select></form>')($rootScope).find('select');
expect(select.attr('name')).toBe('testing-select');
});
});
describe('view->model', function() {
it('should do nothing if clicking selected option', function() {
$rootScope.model = 3;
var el = setupSelect('ng-model="$root.model"', [1,2,3]);
expect(selectedOptions(el).length).toBe(1);
expect(el.find('md-option').eq(2).attr('selected')).toBe('selected');
el.triggerHandler({
type: 'click',
target: el.find('md-option')[2]
});
expect(selectedOptions(el).length).toBe(1);
expect(el.find('md-option').eq(2).attr('selected')).toBe('selected');
expect($rootScope.model).toBe(3);
});
it('should support the ng-change event', function() {
var changesCalled = false;
$rootScope.onChanges = function() {
changesCalled = true;
};
var selectEl = setupSelect('ng-model="myModel" ng-change="changed()"', [1, 2, 3]).find('md-select');
openSelect(selectEl);
var menuEl = $document.find('md-select-menu');
menuEl.triggerHandler({
type: 'click',
target: menuEl.find('md-option')[1]
});
// FIXME- does not work with minified, jquery
// expect(changesCalled).toBe(true);
});
it('should deselect old and select new on click', function() {
$rootScope.model = 3;
var el = setupSelect('ng-model="$root.model"', [1,2,3]);
expect(selectedOptions(el).length).toBe(1);
expect(el.find('md-option').eq(2).attr('selected')).toBe('selected');
openSelect(el);
clickOption(el, 1);
expect(selectedOptions(el).length).toBe(1);
expect($rootScope.model).toBe(2);
});
it('should keep model value if selected option is removed', function() {
$rootScope.model = 3;
$rootScope.values = [1,2,3];
var el = setupSelect('ng-model="$root.model"', '<md-option ng-repeat="v in values" ng-value="v">{{v}}</md-option>');
expect(selectedOptions(el).length).toBe(1);
expect(el.find('md-option').eq(2).attr('selected')).toBe('selected');
$rootScope.$apply('values.pop()');
expect(selectedOptions(el).length).toBe(0);
expect(el.find('md-option').length).toBe(2);
expect($rootScope.model).toBe(3);
});
it('should select an option that was just added matching the modelValue', function() {
$rootScope.model = 4;
$rootScope.values = [1,2,3];
var el = setupSelect('ng-model="$root.model"', '<md-option ng-repeat="v in values" ng-value="v">{{v}}</md-option>');
expect(selectedOptions(el).length).toBe(0);
$rootScope.$apply('values.unshift(4)');
expect(el.find('md-option').length).toBe(4);
expect(el.find('md-option').eq(0).attr('selected')).toBe('selected');
expect(selectedOptions(el).length).toBe(1);
expect($rootScope.model).toBe(4);
});
it('should allow switching between falsy options', inject(function($rootScope) {
$rootScope.model = false;
var el = setupSelect('ng-model="$root.model"', [false, 0]);
openSelect(el);
clickOption(el, 1);
expect($rootScope.model).toBe(0);
}));
});
});
describe('multiple', function() {
describe('model->view', function() {
it('renders initial model value', function() {
$rootScope.model = [1,3];
var el = setupSelectMultiple('ng-model="$root.model"', [1,2,3,4]);
expect(selectedOptions(el).length).toBe(2);
expect(el.find('md-option').eq(0).attr('selected')).toBe('selected');
expect(el.find('md-option').eq(2).attr('selected')).toBe('selected');
expect($rootScope.model).toEqual([1,3]);
});
it('renders nothing if empty array is set', function() {
$rootScope.model = [];
var el = setupSelectMultiple('ng-model="$root.model"', [1,2,3,4]);
expect(selectedOptions(el).length).toBe(0);
expect($rootScope.model).toEqual([]);
});
it('renders nothing if undefined is set', function() {
$rootScope.model = [1, 2];
var el = setupSelectMultiple('ng-model="$root.model"', [1,2,3,4]);
expect(selectedOptions(el).length).toBe(2);
$rootScope.$apply('model = undefined');
$rootScope.$digest();
expect(selectedOptions(el).length).toBe(0);
});
it('adding a valid value to the model selects its option', function() {
$rootScope.model = [];
var el = setupSelectMultiple('ng-model="$root.model"', [1,2,3,4]);
expect(selectedOptions(el).length).toBe(0);
expect($rootScope.model).toEqual([]);
$rootScope.$apply('model.push(2)');
expect(selectedOptions(el).length).toBe(1);
expect(el.find('md-option').eq(1).attr('selected')).toBe('selected');
expect($rootScope.model).toEqual([2]);
});
it('removing a valid value from the model deselects its option', function() {
$rootScope.model = [2,3];
var el = setupSelectMultiple('ng-model="$root.model"', [1,2,3,4]);
expect(selectedOptions(el).length).toBe(2);
expect($rootScope.model).toEqual([2,3]);
$rootScope.$apply('model.shift()');
expect(selectedOptions(el).length).toBe(1);
expect(el.find('md-option').eq(2).attr('selected')).toBe('selected');
expect($rootScope.model).toEqual([3]);
});
it('deselects all options when setting to an empty model', function() {
$rootScope.model = [2,3];
var el = setupSelectMultiple('ng-model="$root.model"', [1,2,3,4]);
expect(selectedOptions(el).length).toBe(2);
expect($rootScope.model).toEqual([2,3]);
$rootScope.$apply('model = []');
expect(selectedOptions(el).length).toBe(0);
expect($rootScope.model).toEqual([]);
});
it('adding multiple valid values to a model selects their options', function() {
$rootScope.model = [2,3];
var el = setupSelectMultiple('ng-model="$root.model"', [1,2,3,4]);
expect(selectedOptions(el).length).toBe(2);
expect($rootScope.model).toEqual([2,3]);
$rootScope.$apply('model = model.concat([1,4])');
expect(selectedOptions(el).length).toBe(4);
expect(el.find('md-option').eq(0).attr('selected')).toBe('selected');
expect(el.find('md-option').eq(1).attr('selected')).toBe('selected');
expect(el.find('md-option').eq(2).attr('selected')).toBe('selected');
expect(el.find('md-option').eq(3).attr('selected')).toBe('selected');
expect($rootScope.model).toEqual([2,3,1,4]);
});
it('correctly selects and deselects options for complete reassignment of model', function() {
$rootScope.model = [2,4,5,6];
var el = setupSelectMultiple('ng-model="$root.model"', [1,2,3,4,5,6]);
expect(selectedOptions(el).length).toBe(4);
expect($rootScope.model).toEqual([2,4,5,6]);
$rootScope.$apply('model = [1,2,3]');
expect(selectedOptions(el).length).toBe(3);
expect(el.find('md-option').eq(0).attr('selected')).toBe('selected');
expect(el.find('md-option').eq(1).attr('selected')).toBe('selected');
expect(el.find('md-option').eq(2).attr('selected')).toBe('selected');
expect($rootScope.model).toEqual([1,2,3]);
});
it('does not select any options if the models value does not match an option', function() {
$rootScope.model = [];
$rootScope.obj = {};
var el = setupSelectMultiple('ng-model="$root.model"', [1,2,3,4,5,6]);
expect(selectedOptions(el).length).toBe(0);
expect($rootScope.model).toEqual([]);
$rootScope.$apply('model = ["bar", obj]');
expect(selectedOptions(el).length).toBe(0);
expect($rootScope.model).toEqual(["bar", $rootScope.obj]);
});
it('uses track by if given to compare objects', function() {
$rootScope.$apply('model = [{id:2}]');
var el=setupSelectMultiple('ng-model="$root.model" ng-model-options="{trackBy: \'$value.id\'}"',
[{id:1}, {id:2}, {id:3}]);
expect(selectedOptions(el).length).toBe(1);
expect(el.find('md-option').eq(1).attr('selected')).toBe('selected');
$rootScope.$apply('model.push({id: 3}); model.push({id:1}); model.shift();');
expect(selectedOptions(el).length).toBe(2);
expect(el.find('md-option').eq(0).attr('selected')).toBe('selected');
expect(el.find('md-option').eq(2).attr('selected')).toBe('selected');
});
it('uses uid by default to compare objects', function() {
var one = {}, two = {}, three = {};
$rootScope.model = [two];
var el = setupSelectMultiple('ng-model="$root.model"', [one, two, three]);
expect(selectedOptions(el).length).toBe(1);
expect(el.find('md-option').eq(1).attr('selected')).toBe('selected');
$rootScope.$apply('model = [{}]');
expect(selectedOptions(el).length).toBe(0);
});
it('errors the model if model value is truthy and not an array', function() {
$rootScope.model = 'string';
var el = setupSelectMultiple('ng-model="$root.model"', [1,2,3]);
var ngModelCtrl = el.find('md-select').controller('ngModel');
expect(ngModelCtrl.$error['md-multiple']).toBe(true);
$rootScope.$apply('model = []');
expect(ngModelCtrl.$valid).toBe(true);
});
it('does not let an empty array satisfy required', function() {
$rootScope.model = [];
$rootScope.opts = [1, 2, 3, 4];
$compile('<form name="testForm">' +
'<md-select ng-model="model" name="multiSelect" required="required" multiple="multiple">' +
'<md-option ng-repeat="opt in opts" ng-value="opt"></md-option>' +
'</md-select></form>')($rootScope);
$rootScope.$digest();
expect($rootScope.testForm.$valid).toBe(false);
});
it('properly validates required attribute based on available options', function() {
var template =
'<form name="testForm">' +
' <md-select ng-model="model" required="required">' +
' <md-option ng-repeat="opt in opts" ng-value="opt"></md-option>' +
' </md-select>' +
'</form>';
$rootScope.opts = [1, 2, 3, 4];
$compile(template)($rootScope);
// Option 0 is not available; should be false
$rootScope.model = 0;
$rootScope.$digest();
expect($rootScope.testForm.$valid).toBe(false);
// Option 1 is available; should be true
$rootScope.model = 1;
$rootScope.$digest();
expect($rootScope.testForm.$valid).toBe(true);
});
it('properly validates required attribute with object options', function() {
var template =
'<form name="testForm">' +
' <md-select ng-model="model" ng-model-options="{ trackBy: \'$value.id\' }" required="required">' +
' <md-option ng-repeat="opt in opts" ng-value="opt"></md-option>' +
' </md-select>' +
'</form>';
$rootScope.opts = [
{ id: 1, value: 'First' },
{ id: 2, value: 'Second' },
{ id: 3, value: 'Third' },
{ id: 4, value: 'Fourth' }
];
$compile(template)($rootScope);
// There is no value selected yet, so the validation should currently fail.
$rootScope.$digest();
expect($rootScope.testForm.$valid).toBe(false);
// Select any valid option, to confirm that the ngModel properly detects the
// tracked option.
$rootScope.model = $rootScope.opts[0];
$rootScope.$digest();
expect($rootScope.testForm.$valid).toBe(true);
});
it('should keep the form pristine when model is predefined', function() {
$rootScope.model = [1, 2];
$rootScope.opts = [1, 2, 3, 4];
$compile('<form name="testForm">' +
'<md-select ng-model="model" name="multiSelect" multiple="multiple">' +
'<md-option ng-repeat="opt in opts" ng-value="opt"></md-option>' +
'</md-select></form>')($rootScope);
$rootScope.$digest();
$timeout.flush();
expect($rootScope.testForm.$pristine).toBe(true);
});
it('should correctly update the input containers label', function() {
var el = setupSelect('ng-required="isRequired" ng-model="someModel"');
var label = el.find('label');
expect(label).not.toHaveClass('md-required');
$rootScope.$apply('isRequired = true');
expect(label).toHaveClass('md-required');
});
it('should correctly update the input containers label when asterisk is disabled', function() {
var el = setupSelect('ng-required="isRequired" md-no-asterisk ng-model="someModel"');
var label = el.find('label');
expect(label).not.toHaveClass('md-required');
$rootScope.$apply('isRequired = true');
expect(label).not.toHaveClass('md-required');
});
it('correctly adds the .md-no-asterisk class if the attribute is empty', function() {
var el = setupSelect('ng-required="isRequired" md-no-asterisk ng-model="someModel"');
var select = el.find('md-select');
expect(select).toHaveClass('md-no-asterisk');
});
it('correctly adds the .md-no-asterisk class if the attribute is true', function() {
var el = setupSelect('ng-required="isRequired" md-no-asterisk ng-model="someModel"');
var select = el.find('md-select');
expect(select).toHaveClass('md-no-asterisk');
});
it('correctly removes the .md-no-asterisk class if the attribute is false', function() {
var el = setupSelect('ng-required="isRequired" md-no-asterisk="false" ng-model="someModel"');
var select = el.find('md-select');
expect(select).not.toHaveClass('md-no-asterisk');
});
});
describe('view->model', function() {
it('should deselect a selected option on click', function() {
$rootScope.model = [1];
var el = setupSelectMultiple('ng-model="$root.model"', [1,2]);
expect(selectedOptions(el).length).toBe(1);
expect($rootScope.model).toEqual([1]);
openSelect(el);
clickOption(el, 0);
expect(selectedOptions(el).length).toBe(0);
expect($rootScope.model).toEqual([]);
});
it('selects a deselected option on click', function() {
$rootScope.model = [1];
var el = setupSelectMultiple('ng-model="$root.model"', [1,2]);
expect(selectedOptions(el).length).toBe(1);
expect($rootScope.model).toEqual([1]);
openSelect(el);
clickOption(el, 1);
expect(selectedOptions(el).length).toBe(2);
expect($rootScope.model).toEqual([1,2]);
});
it('should keep model value if a selected option is removed', function() {
$rootScope.model = [1];
$rootScope.values = [1,2];
var el = setupSelectMultiple('ng-model="$root.model"',
'<md-option ng-repeat="v in values" ng-value="v">{{v}}</md-option>');
expect(selectedOptions(el).length).toBe(1);
expect($rootScope.model).toEqual([1]);
$rootScope.$apply('values.shift()');
expect(selectedOptions(el).length).toBe(0);
expect($rootScope.model).toEqual([1]);
});
it('should select an option that was just added matching the modelValue', function() {
$rootScope.model = [1,3];
$rootScope.values = [1,2];
var el = setupSelectMultiple('ng-model="$root.model"',
'<md-option ng-repeat="v in values" ng-value="v">{{v}}</md-option>');
expect(selectedOptions(el).length).toBe(1);
expect($rootScope.model).toEqual([1,3]);
$rootScope.$apply('values.push(3)');
expect(selectedOptions(el).length).toBe(2);
expect(el.find('md-option').eq(0).attr('selected')).toBe('selected');
expect(el.find('md-option').eq(2).attr('selected')).toBe('selected');
expect($rootScope.model).toEqual([1,3]);
});
it('should not be multiple if attr.multiple == `false`', function() {
var el = setupSelect('multiple="false" ng-model="$root.model"').find('md-select');
openSelect(el);
expectSelectOpen(el);
var selectMenu = $document.find('md-select-menu')[0];
expect(selectMenu.hasAttribute('multiple')).toBe(false);
});
});
});
describe('aria', function() {
var el;
beforeEach(function() {
el = setupSelect('ng-model="someModel"', [1, 2, 3]).find('md-select');
var selectMenus = $document.find('md-select-menu');
selectMenus.remove();
});
it('adds an aria-label from placeholder', function() {
var select = setupSelect('ng-model="someVal" placeholder="Hello world"', null, true).find('md-select');
expect(select.attr('aria-label')).toBe('Hello world');
});
it('preserves aria-label on value change', function() {
var select = $compile('<md-input-container>' +
'<label>Pick</label>' +
'<md-select ng-model="val">' +
'<md-option value="1">One</md-option>' +
'<md-option value="2">Two</md-option>' +
'<md-option value="3">Three</md-option>' +
'</md-select>' +
'</md-input-container>')($rootScope).find('md-select');
$rootScope.$apply('model = 1');
$rootScope.$digest();
expect(select.attr('aria-label')).toBe('Pick');
});
it('preserves existing aria-label', function() {
var select = setupSelect('ng-model="someVal" aria-label="Hello world" placeholder="Pick"').find('md-select');
expect(select.attr('aria-label')).toBe('Hello world');
});
it('should expect an aria-label if none is present', inject(function($log) {
spyOn($log, 'warn');
setupSelect('ng-model="someVal"', null, true).find('md-select');
$rootScope.$apply();
expect($log.warn).toHaveBeenCalled();
$log.warn.calls.reset();
setupSelect('ng-model="someVal", aria-label="Hello world"').find('md-select');
$rootScope.$apply();
expect($log.warn).not.toHaveBeenCalled();
}));
it('sets up the aria-expanded attribute', function() {
expect(el.attr('aria-expanded')).toBe('false');
openSelect(el);
expect(el.attr('aria-expanded')).toBe('true');
closeSelect(el);
$material.flushInterimElement();
expect(el.attr('aria-expanded')).toBe('false');
});
it('sets up the aria-multiselectable attribute', function() {
$rootScope.model = [1,3];
var el = setupSelectMultiple('ng-model="$root.model"', [1,2,3]).find('md-select');
expect(el.attr('aria-multiselectable')).toBe('true');
});
it('sets up the aria-selected attribute', function() {
var el = setupSelect('ng-model="$root.model"', [1,2,3]);
var options = el.find('md-option');
openSelect(el);
expect(options.eq(2).attr('aria-selected')).toBe('false');
clickOption(el, 2);
expect(options.eq(2).attr('aria-selected')).toBe('true');
});
});
describe('keyboard controls', function() {
afterEach(function() {
var selectMenus = $document.find('md-select-menu');
selectMenus.remove();
});
describe('md-select', function() {
it('can be opened with a space key', function() {
var el = setupSelect('ng-model="someModel"', [1, 2, 3]).find('md-select');
pressKey(el, 32);
$material.flushInterimElement();
expectSelectOpen(el);
});
it('can be opened with an enter key', function() {
var el = setupSelect('ng-model="someModel"', [1, 2, 3]).find('md-select');
pressKey(el, 13);
$material.flushInterimElement();
expectSelectOpen(el);
});
it('can be opened with the up key', function() {
var el = setupSelect('ng-model="someModel"', [1, 2, 3]).find('md-select');
pressKey(el, 38);
$material.flushInterimElement();
expectSelectOpen(el);
});
it('can be opened with the down key', function() {
var el = setupSelect('ng-model="someModel"', [1, 2, 3]).find('md-select');
pressKey(el, 40);
$material.flushInterimElement();
expectSelectOpen(el);
});
it('supports typing an option name', function() {
var el = setupSelect('ng-model="someModel"', [1, 2, 3]).find('md-select');
pressKey(el, 50);
expect($rootScope.someModel).toBe(2);
});
it('disallows selection of disabled options', function() {
var optsTemplate =
'<md-option value="1">1</md-option>' +
'<md-option value="2" ng-disabled="true">2</md-option>';
var el = setupSelect('ng-model="someModel"', optsTemplate).find('md-select');
pressKey(el, 50);
expect($rootScope.someModel).toBe(undefined);
});
});
describe('md-select-menu', function() {
it('can be closed with escape', function() {
var el = setupSelect('ng-model="someVal"', [1, 2, 3]).find('md-select');
openSelect(el);
expectSelectOpen(el);
var selectMenu = $document.find('md-select-menu');
expect(selectMenu.length).toBe(1);
pressKey(selectMenu, 27);
$material.flushInterimElement();
expectSelectClosed(el);
});
});
});
function setupSelect(attrs, options, skipLabel, scope, optCompileOpts) {
var el;
var template = '' +
'<md-input-container>' +
(skipLabel ? '' : '<label>Label</label>') +
'<md-select ' + (attrs || '') + '>' +
optTemplate(options, optCompileOpts) +
'</md-select>' +
'</md-input-container>';
el = $compile(template)(scope || $rootScope);
$rootScope.$digest();
attachedElements.push(el);
return el;
}
function setupSelectMultiple(attrs, options, skipLabel, scope) {
attrs = (attrs || '') + ' multiple';
return setupSelect(attrs, options, skipLabel, scope);
}
function optTemplate(options, compileOpts) {
var optionsTpl = '';
if (angular.isArray(options)) {
$rootScope.$$values = options;
var renderValueAs = compileOpts ? compileOpts.renderValueAs || 'value' : 'value';
optionsTpl = '<md-option ng-repeat="value in $$values" ng-value="value"><div class="md-text">{{' + renderValueAs + '}}</div></md-option>';
} else if (angular.isString(options)) {
optionsTpl = options;
}
return optionsTpl;
}
function selectedOptions(el) {
var querySelector = 'md-option[selected]';
var res = angular.element($document[0].querySelectorAll(querySelector));
if (!res.length) {
res = angular.element(el[0].querySelectorAll(querySelector));
}