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,429 lines (1,065 loc) • 56 kB
JavaScript
describe('<md-chips>', function() {
var attachedElements = [];
var scope, $exceptionHandler, $timeout;
var BASIC_CHIP_TEMPLATE =
'<md-chips ng-model="items"></md-chips>';
var CHIP_TRANSFORM_TEMPLATE =
'<md-chips ng-model="items" md-transform-chip="transformChip($chip)"></md-chips>';
var CHIP_ADD_TEMPLATE =
'<md-chips ng-model="items" md-on-add="addChip($chip, $index)"></md-chips>';
var CHIP_REMOVE_TEMPLATE =
'<md-chips ng-model="items" md-on-remove="removeChip($chip, $index)"></md-chips>';
var CHIP_SELECT_TEMPLATE =
'<md-chips ng-model="items" md-on-select="selectChip($chip)"></md-chips>';
var CHIP_READONLY_TEMPLATE =
'<md-chips ng-model="items" readonly="isReadonly"></md-chips>';
var CHIP_READONLY_AUTOCOMPLETE_TEMPLATE =
'<md-chips ng-model="items" readonly="true">' +
' <md-autocomplete md-items="item in [\'hi\', \'ho\', \'he\']"></md-autocomplete>' +
'</md-chips>';
var CHIP_NOT_REMOVABLE_TEMPLATE =
'<md-chips ng-model="items" readonly="true" md-removable="false"></md-chips>';
var CHIP_APPEND_DELAY_TEMPLATE =
'<md-chips ng-model="items" md-chip-append-delay="800"></md-chips>';
afterEach(function() {
attachedElements.forEach(function(element) {
var scope = element.scope();
scope && scope.$destroy();
element.remove();
});
attachedElements = [];
});
describe('with no overrides', function() {
beforeEach(module('material.components.chips', 'material.components.autocomplete'));
beforeEach(inject(function($rootScope, _$exceptionHandler_, _$timeout_) {
scope = $rootScope.$new();
scope.items = ['Apple', 'Banana', 'Orange'];
$exceptionHandler = _$exceptionHandler_;
$timeout = _$timeout_;
}));
describe('basic functionality', function() {
it('should render a default input element', function() {
var element = buildChips(BASIC_CHIP_TEMPLATE);
var ctrl = element.controller('mdChips');
var input = element.find('input');
expect(input.length).toBe(1);
});
it('should render a list of chips', function() {
var element = buildChips(BASIC_CHIP_TEMPLATE);
var chips = getChipElements(element);
expect(chips.length).toBe(3);
expect(chips[0].innerHTML).toContain('Apple');
expect(chips[1].innerHTML).toContain('Banana');
expect(chips[2].innerHTML).toContain('Orange');
});
it('should render a user-provided chip template', function() {
var template =
'<md-chips ng-model="items">' +
' <md-chip-template><div class="mychiptemplate">{$chip}</div></md-chip-template>' +
'</md-chips>';
var element = buildChips(template);
var chip = element.find('md-chip');
expect(chip[0].querySelector('div.mychiptemplate')).not.toBeNull();
});
it('should add a chip', function() {
var element = buildChips(BASIC_CHIP_TEMPLATE);
var ctrl = element.controller('mdChips');
element.scope().$apply(function() {
ctrl.chipBuffer = 'Grape';
simulateInputEnterKey(ctrl);
});
expect(scope.items.length).toBe(4);
var chips = getChipElements(element);
expect(chips.length).toBe(4);
expect(chips[0].innerHTML).toContain('Apple');
expect(chips[1].innerHTML).toContain('Banana');
expect(chips[2].innerHTML).toContain('Orange');
expect(chips[3].innerHTML).toContain('Grape');
});
it('should not add a blank chip', function() {
var element = buildChips(BASIC_CHIP_TEMPLATE);
var ctrl = element.controller('mdChips');
element.scope().$apply(function() {
ctrl.chipBuffer = '';
simulateInputEnterKey(ctrl);
});
expect(scope.items.length).toBe(3);
});
it('should remove a chip', function() {
var element = buildChips(BASIC_CHIP_TEMPLATE);
var ctrl = element.controller('mdChips');
element.scope().$apply(function() {
// Remove "Banana"
ctrl.removeChip(1);
});
var chips = getChipElements(element);
expect(chips.length).toBe(2);
expect(chips[0].innerHTML).toContain('Apple');
expect(chips[1].innerHTML).toContain('Orange');
});
it('should call the transform method when adding a chip', function() {
var element = buildChips(CHIP_TRANSFORM_TEMPLATE);
var ctrl = element.controller('mdChips');
var doubleText = function(text) {
return "" + text + text;
};
scope.transformChip = jasmine.createSpy('transformChip').and.callFake(doubleText);
element.scope().$apply(function() {
ctrl.chipBuffer = 'Grape';
simulateInputEnterKey(ctrl);
});
expect(scope.transformChip).toHaveBeenCalled();
expect(scope.transformChip.calls.mostRecent().args[0]).toBe('Grape');
expect(scope.items.length).toBe(4);
expect(scope.items[3]).toBe('GrapeGrape');
});
it('should not add the chip if md-transform-chip returns null', function() {
var element = buildChips(CHIP_TRANSFORM_TEMPLATE);
var ctrl = element.controller('mdChips');
var nullChip = function(text) {
return null;
};
scope.transformChip = jasmine.createSpy('transformChip').and.callFake(nullChip);
element.scope().$apply(function() {
ctrl.chipBuffer = 'Grape';
simulateInputEnterKey(ctrl);
});
expect(scope.transformChip).toHaveBeenCalled();
expect(scope.transformChip.calls.mostRecent().args[0]).toBe('Grape');
expect(scope.items.length).toBe(3);
});
it('should call the add method when adding a chip', function() {
var element = buildChips(CHIP_ADD_TEMPLATE);
var ctrl = element.controller('mdChips');
scope.addChip = jasmine.createSpy('addChip');
element.scope().$apply(function() {
ctrl.chipBuffer = 'Grape';
simulateInputEnterKey(ctrl);
});
expect(scope.addChip).toHaveBeenCalled();
expect(scope.addChip.calls.mostRecent().args[0]).toBe('Grape'); // Chip
expect(scope.addChip.calls.mostRecent().args[1]).toBe(3); // Index
});
it('should call the remove method when removing a chip', function() {
var element = buildChips(CHIP_REMOVE_TEMPLATE);
var ctrl = element.controller('mdChips');
scope.removeChip = jasmine.createSpy('removeChip');
element.scope().$apply(function() {
ctrl.items = ['Grape'];
ctrl.removeChip(0);
});
expect(scope.removeChip).toHaveBeenCalled();
expect(scope.removeChip.calls.mostRecent().args[0]).toBe('Grape'); // Chip
expect(scope.removeChip.calls.mostRecent().args[1]).toBe(0); // Index
});
it('should call the select method when selecting a chip', function() {
var element = buildChips(CHIP_SELECT_TEMPLATE);
var ctrl = element.controller('mdChips');
scope.selectChip = jasmine.createSpy('selectChip');
element.scope().$apply(function() {
ctrl.items = ['Grape'];
ctrl.selectChip(0);
});
expect(scope.selectChip).toHaveBeenCalled();
expect(scope.selectChip.calls.mostRecent().args[0]).toBe('Grape');
});
describe('when adding chips on blur', function() {
it('should append a new chip for the remaining text', function() {
var element = buildChips(
'<md-chips ng-model="items" md-add-on-blur="true">' +
'</md-chips>'
);
var input = element.find('input');
expect(scope.items.length).toBe(3);
input.val('Remaining');
input.triggerHandler('change');
// Trigger a blur event, to check if the text was converted properly.
input.triggerHandler('blur');
expect(scope.items.length).toBe(4);
});
it('should not append a new chip if the limit has reached', function() {
var element = buildChips(
'<md-chips ng-model="items" md-add-on-blur="true" md-max-chips="3">' +
'</md-chips>'
);
var input = element.find('input');
expect(scope.items.length).toBe(3);
input.val('Remaining');
input.triggerHandler('change');
// Trigger a blur event, to check if the text was converted properly.
input.triggerHandler('blur');
expect(scope.items.length).toBe(3);
});
it('should not append a new chip when the chips model is invalid', function() {
var element = buildChips(
'<md-chips ng-model="items" md-add-on-blur="true">'
);
var input = element.find('input');
var ngModelCtrl = element.controller('ngModel');
expect(scope.items.length).toBe(3);
input.val('Remaining');
input.triggerHandler('change');
input.triggerHandler('blur');
$timeout.flush();
expect(scope.items.length).toBe(4);
input.val('Second');
ngModelCtrl.$setValidity('is-valid', false);
input.triggerHandler('change');
input.triggerHandler('blur');
expect(scope.items.length).toBe(4);
});
it('should not append a new chip when the custom input model is invalid', function() {
var element = buildChips(
'<md-chips ng-model="items" md-add-on-blur="true">' +
'<input ng-model="subModel" ng-maxlength="2">' +
'</md-chips>'
);
$timeout.flush();
var input = element.find('input');
expect(scope.items.length).toBe(3);
input.val('EN');
input.triggerHandler('change');
input.triggerHandler('blur');
// Flush the timeout after each blur, because custom inputs have listeners running
// in an Angular digest.
$timeout.flush();
expect(scope.items.length).toBe(4);
input.val('Another');
input.triggerHandler('change');
input.triggerHandler('blur');
// Flush the timeout after each blur, because custom inputs have listeners running
// in an Angular digest.
$timeout.flush();
expect(scope.items.length).toBe(4);
});
it('should not append a new chip when requireMatch is enabled', function() {
var template =
'<md-chips ng-model="items" md-add-on-blur="true" md-require-match="true">' +
'<md-autocomplete ' +
'md-selected-item="selectedItem" ' +
'md-search-text="searchText" ' +
'md-items="item in querySearch(searchText)" ' +
'md-item-text="item">' +
'<span md-highlight-text="searchText">{{item}}</span>' +
'</md-autocomplete>' +
'</md-chips>';
setupScopeForAutocomplete();
var element = buildChips(template);
var ctrl = element.controller('mdChips');
var input = element.find('input');
expect(ctrl.shouldAddOnBlur()).toBeFalsy();
// Flush the initial timeout of the md-autocomplete.
$timeout.flush();
scope.$apply('searchText = "Hello"');
expect(ctrl.shouldAddOnBlur()).toBeFalsy();
});
it('should not append a new chip on blur when the autocomplete is showing', function() {
var template =
'<md-chips ng-model="items" md-add-on-blur="true">' +
'<md-autocomplete ' +
'md-selected-item="selectedItem" ' +
'md-search-text="searchText" ' +
'md-items="item in querySearch(searchText)" ' +
'md-item-text="item">' +
'<span md-highlight-text="searchText">{{item}}</span>' +
'</md-autocomplete>' +
'</md-chips>';
setupScopeForAutocomplete();
var element = buildChips(template);
var ctrl = element.controller('mdChips');
var input = element.find('input');
expect(ctrl.shouldAddOnBlur()).toBeFalsy();
// Flush the initial timeout of the md-autocomplete.
$timeout.flush();
var autocompleteCtrl = element.find('md-autocomplete').controller('mdAutocomplete');
// Open the dropdown by searching for a possible item and focusing the input.
scope.$apply('searchText = "Ki"');
autocompleteCtrl.focus();
expect(ctrl.shouldAddOnBlur()).toBeFalsy();
});
});
describe('when removable', function() {
it('should not append the input div when not removable and readonly is enabled', function() {
var element = buildChips(CHIP_NOT_REMOVABLE_TEMPLATE);
var wrap = element.children();
var controller = element.controller("mdChips");
expect(wrap.hasClass("md-removable")).toBe(false);
expect(controller.removable).toBe(false);
var containers = wrap[0].querySelectorAll(".md-chip-input-container");
expect(containers.length).toBe(0);
var removeContainer = wrap[0].querySelector('.md-chip-remove-container');
expect(removeContainer).not.toBeTruthy();
});
it('should not remove chip through the backspace/delete key when removable is set to false', inject(function($mdConstant) {
var element = buildChips(CHIP_NOT_REMOVABLE_TEMPLATE);
var wrap = element.find('md-chips-wrap');
var controller = element.controller("mdChips");
var chips = getChipElements(element);
expect(wrap.hasClass("md-removable")).toBe(false);
expect(controller.removable).toBe(false);
controller.selectChip(0);
wrap.triggerHandler({
type: 'keydown',
keyCode: $mdConstant.KEY_CODE.BACKSPACE
});
var updatedChips = getChipElements(element);
expect(chips.length).toBe(updatedChips.length);
}));
it('should remove a chip by default through the backspace/delete key', inject(function($mdConstant) {
var element = buildChips(BASIC_CHIP_TEMPLATE);
var wrap = element.find('md-chips-wrap');
var controller = element.controller("mdChips");
var chips = getChipElements(element);
controller.selectChip(0);
wrap.triggerHandler({
type: 'keydown',
keyCode: $mdConstant.KEY_CODE.BACKSPACE
});
var updatedChips = getChipElements(element);
expect(updatedChips.length).toBe(chips.length - 1);
}));
it('should set removable to true by default', function() {
var element = buildChips(BASIC_CHIP_TEMPLATE);
var wrap = element.children();
var controller = element.controller('mdChips');
expect(wrap.hasClass('md-removable')).toBe(true);
// The controller variable is kept undefined by default, to allow us to difference between the default value
// and a user-provided value.
expect(controller.removable).toBe(undefined);
var containers = wrap[0].querySelectorAll(".md-chip-input-container");
expect(containers.length).not.toBe(0);
var removeContainer = wrap[0].querySelector('.md-chip-remove-container');
expect(removeContainer).toBeTruthy();
});
it('should append dynamically the remove button', function() {
var template = '<md-chips ng-model="items" readonly="true" md-removable="removable"></md-chips>';
scope.removable = false;
var element = buildChips(template);
var wrap = element.children();
var controller = element.controller("mdChips");
expect(wrap.hasClass("md-removable")).toBe(false);
expect(controller.removable).toBe(false);
var containers = wrap[0].querySelectorAll(".md-chip-remove-container");
expect(containers.length).toBe(0);
scope.$apply('removable = true');
expect(wrap.hasClass("md-removable")).toBe(true);
expect(controller.removable).toBe(true);
containers = wrap[0].querySelector(".md-chip-remove-container");
expect(containers).toBeTruthy();
});
});
describe('when readonly', function() {
var element, ctrl;
it("properly toggles the controller's readonly property", function() {
element = buildChips(CHIP_READONLY_TEMPLATE);
ctrl = element.controller('mdChips');
expect(ctrl.readonly).toBeFalsy();
scope.$apply('isReadonly = true');
expect(ctrl.readonly).toBeTruthy();
});
it("properly toggles the wrapper's .md-readonly class", function() {
element = buildChips(CHIP_READONLY_TEMPLATE);
ctrl = element.controller('mdChips');
expect(element.find('md-chips-wrap')).not.toHaveClass('md-readonly');
scope.$apply('isReadonly = true');
expect(element.find('md-chips-wrap')).toHaveClass('md-readonly');
});
it('is false with empty items should not hide the chips wrapper', function() {
scope.isReadonly = false;
scope.items = [];
element = buildChips(CHIP_READONLY_TEMPLATE);
expect(element.find('md-chips-wrap').length).toBe(1);
});
it('is true with empty items should not hide the chips wrapper', function() {
scope.isReadonly = true;
scope.items = [];
element = buildChips(CHIP_READONLY_TEMPLATE);
expect(element.find('md-chips-wrap').length).toBe(1);
});
it('is true should not throw an error when used with an autocomplete', function() {
element = buildChips(CHIP_READONLY_AUTOCOMPLETE_TEMPLATE);
$timeout.flush();
expect($exceptionHandler.errors).toEqual([]);
});
it('should disable removing when `md-removable` is not defined', function() {
element = buildChips(
'<md-chips ng-model="items" readonly="isReadonly" md-removable="isRemovable"></md-chips>'
);
var wrap = element.find('md-chips-wrap');
ctrl = element.controller('mdChips');
expect(element.find('md-chips-wrap')).not.toHaveClass('md-readonly');
scope.$apply('isReadonly = true');
expect(element.find('md-chips-wrap')).toHaveClass('md-readonly');
expect(ctrl.removable).toBeUndefined();
var removeContainer = wrap[0].querySelector('.md-chip-remove-container');
expect(removeContainer).toBeFalsy();
scope.$apply('isRemovable = true');
removeContainer = wrap[0].querySelector('.md-chip-remove-container');
expect(removeContainer).toBeTruthy();
});
});
it('should disallow duplicate object chips', function() {
var element = buildChips(CHIP_TRANSFORM_TEMPLATE);
var ctrl = element.controller('mdChips');
// Manually set the items
ctrl.items = [{name: 'Apple', uppername: 'APPLE'}];
// Make our custom transformChip function return our existing item
var chipObj = function(chip) {
return ctrl.items[0];
};
scope.transformChip = jasmine.createSpy('transformChip').and.callFake(chipObj);
element.scope().$apply(function() {
ctrl.chipBuffer = 'Apple';
simulateInputEnterKey(ctrl);
});
expect(ctrl.items.length).toBe(1);
expect(scope.transformChip).toHaveBeenCalled();
expect(scope.transformChip.calls.mostRecent().args[0]).toBe('Apple');
});
it('should disallow identical object chips', function() {
var element = buildChips(CHIP_TRANSFORM_TEMPLATE);
var ctrl = element.controller('mdChips');
ctrl.items = [{name: 'Apple', uppername: 'APPLE'}];
var chipObj = function(chip) {
return {
name: chip,
uppername: chip.toUpperCase()
};
};
scope.transformChip = jasmine.createSpy('transformChip').and.callFake(chipObj);
element.scope().$apply(function() {
ctrl.chipBuffer = 'Apple';
simulateInputEnterKey(ctrl);
});
expect(ctrl.items.length).toBe(1);
expect(scope.transformChip).toHaveBeenCalled();
expect(scope.transformChip.calls.mostRecent().args[0]).toBe('Apple');
});
it('should prevent the default when backspace is pressed', inject(function($mdConstant) {
var element = buildChips(BASIC_CHIP_TEMPLATE);
var ctrl = element.controller('mdChips');
var backspaceEvent = {
type: 'keydown',
keyCode: $mdConstant.KEY_CODE.BACKSPACE,
which: $mdConstant.KEY_CODE.BACKSPACE,
preventDefault: jasmine.createSpy('preventDefault')
};
element.find('input').triggerHandler(backspaceEvent);
expect(backspaceEvent.preventDefault).toHaveBeenCalled();
}));
describe('with input text', function() {
it('should prevent the default when enter is pressed', inject(function($mdConstant) {
var element = buildChips(BASIC_CHIP_TEMPLATE);
var ctrl = element.controller('mdChips');
var enterEvent = {
type: 'keydown',
keyCode: $mdConstant.KEY_CODE.ENTER,
which: $mdConstant.KEY_CODE.ENTER,
preventDefault: jasmine.createSpy('preventDefault')
};
ctrl.chipBuffer = 'Test';
element.find('input').triggerHandler(enterEvent);
expect(enterEvent.preventDefault).toHaveBeenCalled();
}));
it('should trim the buffer when a chip will be added', inject(function($mdConstant) {
var element = buildChips(BASIC_CHIP_TEMPLATE);
var ctrl = element.controller('mdChips');
var input = element.find('input');
// This string contains a lot of spaces, which should be trimmed.
input.val(' Test ');
// We have to trigger the `change` event, because IE11 does not support
// the `input` event to update the ngModel. An alternative for `input` is to use the `change` event.
input.triggerHandler('change');
expect(ctrl.chipBuffer).toBeTruthy();
var enterEvent = {
type: 'keydown',
keyCode: $mdConstant.KEY_CODE.ENTER,
which: $mdConstant.KEY_CODE.ENTER
};
input.triggerHandler(enterEvent);
expect(scope.items).toEqual(['Apple', 'Banana', 'Orange', 'Test']);
}));
describe('with backspace event', function() {
var backspaceEvent, element, input, ctrl, isValidInput;
beforeEach(inject(function($mdConstant) {
backspaceEvent = {
type: 'keydown',
keyCode: $mdConstant.KEY_CODE.BACKSPACE,
which: $mdConstant.KEY_CODE.BACKSPACE,
preventDefault: jasmine.createSpy('preventDefault')
};
}));
afterEach(function() {
element && element.remove();
});
function createChips(template) {
element = buildChips(template);
input = element.find('input');
ctrl = element.controller('mdChips');
$timeout.flush();
// Add the element to the document's body, because otherwise we won't be able
// to set the selection of the chip input.
document.body.appendChild(element[0]);
/** Detect whether the current input is supporting the `selectionStart` property */
var oldInputValue = input.val();
input.val('2');
isValidInput = angular.isDefined(ctrl.getCursorPosition(input[0]));
input.val(oldInputValue);
}
/**
* Updates the cursor position of the input.
* This is necessary to test the cursor position.
*/
function updateInputCursor() {
if (isValidInput) {
input[0].selectionStart = input[0].selectionEnd = input[0].value.length;
}
}
it('should properly cancel the backspace event to select the chip before', function() {
createChips(BASIC_CHIP_TEMPLATE);
input.val(' ');
updateInputCursor();
input.triggerHandler('change');
input.triggerHandler(backspaceEvent);
expect(backspaceEvent.preventDefault).not.toHaveBeenCalled();
input.val('');
updateInputCursor();
input.triggerHandler('change');
input.triggerHandler(backspaceEvent);
expect(backspaceEvent.preventDefault).toHaveBeenCalledTimes(1);
});
it('should properly cancel the backspace event to select the chip before', function() {
createChips(BASIC_CHIP_TEMPLATE);
input.val(' ');
updateInputCursor();
input.triggerHandler('change');
input.triggerHandler(backspaceEvent);
expect(backspaceEvent.preventDefault).not.toHaveBeenCalled();
input.val('');
updateInputCursor();
input.triggerHandler('change');
input.triggerHandler(backspaceEvent);
expect(backspaceEvent.preventDefault).toHaveBeenCalledTimes(1);
});
it('should properly handle the cursor position when using a number input', function() {
createChips(
'<md-chips ng-model="items">' +
'<input type="number" placeholder="Enter a number">' +
'</md-chips>'
);
input.val('2');
updateInputCursor();
input.triggerHandler('change');
input.triggerHandler(backspaceEvent);
$timeout.flush();
expect(backspaceEvent.preventDefault).not.toHaveBeenCalled();
input.val('');
updateInputCursor();
input.triggerHandler('change');
input.triggerHandler(backspaceEvent);
$timeout.flush();
expect(backspaceEvent.preventDefault).toHaveBeenCalledTimes(1);
});
});
});
it('focuses/blurs the component when focusing/blurring the input', inject(function() {
var element = buildChips(BASIC_CHIP_TEMPLATE);
var ctrl = element.controller('mdChips');
// Focus the input and check
element.find('input').triggerHandler('focus');
expect(ctrl.inputHasFocus).toBe(true);
expect(element.find('md-chips-wrap').hasClass('md-focused')).toBe(true);
// Blur the input and check
element.find('input').triggerHandler('blur');
expect(ctrl.inputHasFocus).toBe(false);
expect(element.find('md-chips-wrap').hasClass('md-focused')).toBe(false);
}));
describe('placeholder', function() {
it('should put placeholder text in the input element when chips exist but there is no secondary-placeholder text', inject(function() {
var template =
'<md-chips ng-model="items" placeholder="placeholder text"></md-chips>';
var element = buildChips(template);
var ctrl = element.controller('mdChips');
var input = element.find('input')[0];
expect(scope.items.length).toBeGreaterThan(0);
expect(input.placeholder).toBe('placeholder text');
}));
it('should put placeholder text in the input element when there are no chips', inject(function() {
var ctrl, element, input, template;
scope.items = [];
template =
'<md-chips ng-model="items" placeholder="placeholder text" ' +
'secondary-placeholder="secondary-placeholder text"></md-chips>';
element = buildChips(template);
ctrl = element.controller('mdChips');
input = element.find('input')[0];
expect(scope.items.length).toBe(0);
expect(input.placeholder).toBe('placeholder text');
}));
it('should put secondary-placeholder text in the input element when there is at least one chip', inject(function() {
var template =
'<md-chips ng-model="items" placeholder="placeholder text" ' +
'secondary-placeholder="secondary-placeholder text"></md-chips>';
var element = buildChips(template);
var ctrl = element.controller('mdChips');
var input = element.find('input')[0];
expect(scope.items.length).toBeGreaterThan(0);
expect(input.placeholder).toBe('secondary-placeholder text');
}));
});
it('utilizes the default chip append delay of 300ms', inject(function($timeout) {
var element = buildChips(BASIC_CHIP_TEMPLATE);
var ctrl = element.controller('mdChips');
// Append element to body
angular.element(document.body).append(element);
// Append a new chips which will fire the delay
ctrl.appendChip('test');
// Before 300ms timeout, focus should be on the chip (i.e. the chip content)
$timeout.flush(299);
expect(document.activeElement).toHaveClass('md-chip-content');
// At/after 300ms timeout, focus should be on the input
$timeout.flush(1);
expect(document.activeElement.tagName.toUpperCase()).toEqual('INPUT');
// cleanup
element.remove();
}));
it('utilizes a custom chip append delay', inject(function($timeout) {
var element = buildChips(CHIP_APPEND_DELAY_TEMPLATE);
var ctrl = element.controller('mdChips');
// Append element to body
angular.element(document.body).append(element);
// Append a new chips which will fire the delay
ctrl.appendChip('test');
// Before custom timeout, focus should be on the chip (i.e. the chip content)
$timeout.flush(ctrl.chipAppendDelay - 1);
expect(document.activeElement).toHaveClass('md-chip-content');
// At/after custom timeout, focus should be on the input
$timeout.flush(1);
expect(document.activeElement.tagName.toUpperCase()).toEqual('INPUT');
// cleanup
element.remove();
}));
});
describe('custom inputs', function() {
describe('separator-keys', function() {
var SEPARATOR_KEYS_CHIP_TEMPLATE =
'<md-chips ng-model="items" md-separator-keys="keys"></md-chips>';
it('should create a new chip when a comma is entered', inject(function($mdConstant) {
scope.keys = [$mdConstant.KEY_CODE.ENTER, $mdConstant.KEY_CODE.COMMA];
var element = buildChips(SEPARATOR_KEYS_CHIP_TEMPLATE);
var ctrl = element.controller('mdChips');
var commaInput = {
type: 'keydown',
keyCode: $mdConstant.KEY_CODE.COMMA,
which: $mdConstant.KEY_CODE.COMMA,
preventDefault: jasmine.createSpy('preventDefault')
};
ctrl.chipBuffer = 'Test';
element.find('input').triggerHandler(commaInput);
expect(commaInput.preventDefault).toHaveBeenCalled();
}));
it('supports custom separator key codes', inject(function($mdConstant) {
var semicolon = 186;
scope.keys = [$mdConstant.KEY_CODE.ENTER, $mdConstant.KEY_CODE.COMMA, semicolon];
var element = buildChips(SEPARATOR_KEYS_CHIP_TEMPLATE);
var ctrl = element.controller('mdChips');
var semicolonInput = {
type: 'keydown',
keyCode: semicolon,
which: semicolon,
preventDefault: jasmine.createSpy('preventDefault')
};
ctrl.chipBuffer = 'Test';
element.find('input').triggerHandler(semicolonInput);
expect(semicolonInput.preventDefault).toHaveBeenCalled();
}));
});
describe('md-max-chips', function() {
beforeEach(function() {
// Clear default items to test the max chips functionality
scope.items = [];
});
it('should not add a new chip if the max-chips limit is reached', function () {
var element = buildChips('<md-chips ng-model="items" md-max-chips="1"></md-chips>');
var ctrl = element.controller('mdChips');
element.scope().$apply(function() {
ctrl.chipBuffer = 'Test';
simulateInputEnterKey(ctrl);
});
expect(scope.items.length).toBe(1);
element.scope().$apply(function() {
ctrl.chipBuffer = 'Test 2';
simulateInputEnterKey(ctrl);
});
expect(scope.items.length).not.toBe(2);
});
it('should update the md-max-chips model validator for forms', function() {
var template =
'<form name="form">' +
'<md-chips name="chips" ng-model="items" md-max-chips="1"></md-chips>' +
'</form>';
var element = buildChips(template);
var ctrl = element.find('md-chips').controller('mdChips');
element.scope().$apply(function() {
ctrl.chipBuffer = 'Test';
simulateInputEnterKey(ctrl);
});
expect(scope.form.chips.$error['md-max-chips']).toBe(true);
});
it('should not reset the buffer if the maximum is reached', function() {
var element = buildChips('<md-chips ng-model="items" md-max-chips="1"></md-chips>');
var ctrl = element.controller('mdChips');
element.scope().$apply(function() {
ctrl.chipBuffer = 'Test';
simulateInputEnterKey(ctrl);
});
expect(scope.items.length).toBe(1);
element.scope().$apply(function() {
ctrl.chipBuffer = 'Test 2';
simulateInputEnterKey(ctrl);
});
expect(ctrl.chipBuffer).toBe('Test 2');
expect(scope.items.length).not.toBe(2);
});
it('should not append the chip when maximum is reached and using an autocomplete', function() {
var template =
'<md-chips ng-model="items" md-max-chips="1">' +
'<md-autocomplete ' +
'md-selected-item="selectedItem" ' +
'md-search-text="searchText" ' +
'md-items="item in querySearch(searchText)" ' +
'md-item-text="item">' +
'<span md-highlight-text="searchText">{{itemtype}}</span>' +
'</md-autocomplete>' +
'</md-chips>';
setupScopeForAutocomplete();
var element = buildChips(template);
var ctrl = element.controller('mdChips');
// Flush the autocompletes init timeout.
$timeout.flush();
var autocompleteCtrl = element.find('md-autocomplete').controller('mdAutocomplete');
element.scope().$apply(function() {
autocompleteCtrl.scope.searchText = 'K';
});
element.scope().$apply(function() {
autocompleteCtrl.select(0);
});
$timeout.flush();
expect(scope.items.length).toBe(1);
expect(scope.items[0]).toBe('Kiwi');
expect(element.find('input').val()).toBe('');
element.scope().$apply(function() {
autocompleteCtrl.scope.searchText = 'O';
});
element.scope().$apply(function() {
autocompleteCtrl.select(0);
});
$timeout.flush();
expect(scope.items.length).toBe(1);
expect(element.find('input').val()).toBe('Orange');
});
});
describe('focus functionality', function() {
var element, ctrl;
beforeEach(function() {
element = buildChips(CHIP_SELECT_TEMPLATE);
ctrl = element.controller('mdChips');
document.body.appendChild(element[0]);
});
afterEach(function() {
element.remove();
element = ctrl = null;
});
it('should focus the chip when clicking / touching on the chip', function() {
ctrl.focusChip = jasmine.createSpy('focusChipSpy');
var chips = getChipElements(element);
expect(chips.length).toBe(3);
chips.children().eq(0).triggerHandler('click');
expect(ctrl.focusChip).toHaveBeenCalledTimes(1);
});
it('should focus the chip through normal content focus', function() {
scope.selectChip = jasmine.createSpy('focusChipSpy');
var chips = getChipElements(element);
expect(chips.length).toBe(3);
chips.children().eq(0).triggerHandler('focus');
expect(scope.selectChip).toHaveBeenCalledTimes(1);
});
it('should blur the chip correctly', function() {
var chips = getChipElements(element);
expect(chips.length).toBe(3);
var chipContent = chips.children().eq(0);
chipContent.triggerHandler('focus');
expect(ctrl.selectedChip).toBe(0);
chipContent.eq(0).triggerHandler('blur');
scope.$digest();
expect(ctrl.selectedChip).toBe(-1);
});
});
describe('md-autocomplete', function() {
var AUTOCOMPLETE_CHIPS_TEMPLATE = '\
<md-chips ng-model="items">\
<md-autocomplete\
md-selected-item="selectedItem"\
md-search-text="searchText"\
md-items="item in querySearch(searchText)"\
md-item-text="item">\
<span md-highlight-text="searchText">{{itemtype}}</span>\
</md-autocomplete>\
</md-chips>';
it('should use the selected item as a buffer', inject(function($timeout) {
setupScopeForAutocomplete();
var element = buildChips(AUTOCOMPLETE_CHIPS_TEMPLATE);
var ctrl = element.controller('mdChips');
$timeout.flush(); // mdAutcomplete needs a flush for its init.
var autocompleteCtrl = element.find('md-autocomplete').controller('mdAutocomplete');
element.scope().$apply(function() {
autocompleteCtrl.scope.searchText = 'K';
});
element.scope().$apply(function() {
autocompleteCtrl.select(0);
});
$timeout.flush();
expect(scope.items.length).toBe(4);
expect(scope.items[3]).toBe('Kiwi');
expect(element.find('input').val()).toBe('');
}));
it('should properly cancel the backspace event to select the chip before', inject(function($mdConstant) {
setupScopeForAutocomplete();
var element = buildChips(AUTOCOMPLETE_CHIPS_TEMPLATE);
// Add the element to the document's body, because otherwise we won't be able
// to set the selection of the chip input.
document.body.appendChild(element[0]);
// The embedded `md-autocomplete` needs a timeout flush for it's initialization.
$timeout.flush();
$timeout.flush();
scope.$apply();
var input = angular.element(element[0].querySelector('md-autocomplete input'));
input.val(' ');
input.triggerHandler('input');
expect(input.controller('ngModel').$modelValue).toBe('');
// Since the `md-chips` component is testing the backspace select previous chip functionality by
// checking the current caret / cursor position, we have to set the cursor to the end of the current
// value.
input[0].selectionStart = input[0].selectionEnd = input[0].value.length;
var backspaceEvent = {
type: 'keydown',
keyCode: $mdConstant.KEY_CODE.BACKSPACE,
which: $mdConstant.KEY_CODE.BACKSPACE,
preventDefault: jasmine.createSpy('preventDefault')
};
input.triggerHandler(backspaceEvent);
// We have to trigger a digest, because the event listeners for the chips component will be called
// with an async digest evaluation.
scope.$digest();
expect(backspaceEvent.preventDefault).not.toHaveBeenCalled();
input.val('');
input.triggerHandler('input');
// Since the `md-chips` component is testing the backspace select previous chip functionality by
// checking the current caret / cursor position, we have to set the cursor to the end of the current
// value.
input[0].selectionStart = input[0].selectionEnd = input[0].value.length;
input.triggerHandler(backspaceEvent);
scope.$digest();
expect(backspaceEvent.preventDefault).toHaveBeenCalledTimes(1);
// Remove the chips element from the document's body.
document.body.removeChild(element[0]);
}));
it('simultaneously allows selecting an existing chip AND adding a new one', inject(function($mdConstant) {
// Setup our scope and function
setupScopeForAutocomplete();
scope.transformChip = jasmine.createSpy('transformChip');
// Modify the base template to add md-transform-chip
var modifiedTemplate = AUTOCOMPLETE_CHIPS_TEMPLATE
.replace('<md-chips', '<md-chips md-transform-chip="transformChip($chip)"');
var element = buildChips(modifiedTemplate);
var ctrl = element.controller('mdChips');
$timeout.flush(); // mdAutcomplete needs a flush for its init.
var autocompleteCtrl = element.find('md-autocomplete').controller('mdAutocomplete');
element.scope().$apply(function() {
autocompleteCtrl.scope.searchText = 'K';
});
autocompleteCtrl.focus();
$timeout.flush();
/*
* Send a down arrow/enter to select the right fruit
*/
var downArrowEvent = {
type: 'keydown',
keyCode: $mdConstant.KEY_CODE.DOWN_ARROW,
which: $mdConstant.KEY_CODE.DOWN_ARROW
};
var enterEvent = {
type: 'keydown',
keyCode: $mdConstant.KEY_CODE.ENTER,
which: $mdConstant.KEY_CODE.ENTER
};
element.find('input').triggerHandler(downArrowEvent);
element.find('input').triggerHandler(enterEvent);
$timeout.flush();
// Check our transformChip calls
expect(scope.transformChip).not.toHaveBeenCalledWith('K');
expect(scope.transformChip).toHaveBeenCalledWith('Kiwi');
expect(scope.transformChip.calls.count()).toBe(1);
// Check our output
expect(scope.items.length).toBe(4);
expect(scope.items[3]).toBe('Kiwi');
expect(element.find('input').val()).toBe('');
// Reset our jasmine spy
scope.transformChip.calls.reset();
/*
* Use the "new chip" functionality
*/
// Set the search text
element.scope().$apply(function() {
autocompleteCtrl.scope.searchText = 'Acai Berry';
});
// Fire our event and flush any timeouts
element.find('input').triggerHandler(enterEvent);
$timeout.flush();
// Check our transformChip calls
expect(scope.transformChip).toHaveBeenCalledWith('Acai Berry');
expect(scope.transformChip.calls.count()).toBe(1);
// Check our output
expect(scope.items.length).toBe(5);
expect(scope.items[4]).toBe('Acai Berry');
expect(element.find('input').val()).toBe('');
}));
it('should remove a chip on click and return focus to the input', function() {
var template =
'<md-chips ng-model="items" md-max-chips="1">' +
'<md-autocomplete ' +
'md-selected-item="selectedItem" ' +
'md-search-text="searchText" ' +
'md-items="item in querySearch(searchText)" ' +
'md-item-text="item">' +
'<span md-highlight-text="searchText">{{itemtype}}</span>' +
'</md-autocomplete>' +
'</md-chips>';
setupScopeForAutocomplete();
var element = buildChips(template);
document.body.appendChild(element[0]);
// Flush the autocomplete's init timeout.
$timeout.flush();
var input = element.find('input');
var removeButton = element[0].querySelector('.md-chip-remove');
expect(scope.items.length).toBe(3);
angular.element(removeButton).triggerHandler('click');
$timeout.flush();
expect(scope.items.length).toBe(2);
expect(document.activeElement).toBe(input[0]);
});
});
describe('user input templates', function() {
var NG_MODEL_TEMPLATE = '\
<md-chips ng-model="items">\
<input type="text" ng-model="inputText">\
</md-chips>';
var INPUT_TEMPLATE = '\
<md-chips ng-model="items">\
<input type="text">\
</md-chips>';
it('focuses/blurs the component when focusing/blurring the input', inject(function($timeout) {
var element = buildChips(INPUT_TEMPLATE);
var ctrl = element.controller('mdChips');
$timeout.flush();
// Focus the input and check
element.find('input').triggerHandler('focus');
$timeout.flush();
expect(ctrl.inputHasFocus).toBe(true);
expect(element.find('md-chips-wrap').hasClass('md-focused')).toBe(true);
// Blur the input and check
element.find('input').triggerHandler('blur');
$timeout.flush();
expect(ctrl.inputHasFocus).toBe(false);
expect(element.find('md-chips-wrap').hasClass('md-focused')).toBe(false);
}));
describe('using ngModel', function() {
it('should add the ngModelCtrl.$viewValue when <enter> is pressed',
inject(function($timeout) {
var element = buildChips(NG_MODEL_TEMPLATE);
var ctrl = element.controller('mdChips');
$timeout.flush();
var ngModelCtrl = ctrl.userInputNgModelCtrl;
element.scope().$apply(function() {
ngModelCtrl.$viewValue = 'Grape';
simulateInputEnterKey(ctrl);
});
expect(scope.items.length).toBe(4);
expect(scope.items[3]).toBe('Grape');
}));
it('should use an empty string if ngModel value is falsy', inject(function($timeout) {
var element = buildChips(NG_MODEL_TEMPLATE);
var ctrl = element.controller('mdChips');
$timeout.flush();
var ngModelCtrl = ctrl.userInputNgModelCtrl;
expect(ngModelCtrl.$viewValue).toBeFalsy();
expect(ctrl.getChipBuffer()).toBe('');
}));
});
describe('without ngModel', function() {
it('should support an input without an ngModel', inject(function($timeout) {
var element = buildChips(INPUT_TEMPLATE);
var ctrl = element.controller('mdChips');
$timeout.flush();
element.scope().$apply(function() {
ctrl.userInputElement[0].value = 'Kiwi';
simulateInputEnterKey(ctrl);
});
expect(scope.items.length).toBe(4);
expect(scope.items[3]).toBe('Kiwi');
}));
});
});
});
describe('static chips', function() {
var STATIC_CHIPS_TEMPLATE = '\
<md-chips>\
<md-chip>Hockey</md-chip>\
<md-chip>Lacrosse</md-chip>\
<md-chip>Baseball</md-chip>\
<md-chip>{{chipItem}}</md-chip>\
</md-chips>';
var STATIC_CHIPS_NGREPEAT_TEMPLATE = '\
<div>\
<div ng-repeat="i in [1,2,3]">\
<md-chips>\
<md-chip>{{i}}</md-chip>\
</md-chips>\
</div>\
</div>\
';
it('should transclude static chips', inject(function($timeout) {
scope.chipItem = 'Football';
var element = buildChips(STATIC_CHIPS_TEMPLATE);
var ctrl = element.controller('mdChips');
$timeout.flush();
var chips = getChipElements(element);
expect(chips.length).toBe(4);
expect(chips[0].innerHTML).toContain('Hockey');
expect(chips[1].innerHTML).toContain('Lacrosse');
expect(chips[2].innerHTML).toContain('Baseball');
expect(chips[3].innerHTML).toContain('Football');
}));
it('allows ng-repeat outside of md-chips', function() {
var element = buildChips(STATIC_CHIPS_NGREPEAT_TEMPLATE);
var ctrl = element.controller('mdChips');
$timeout.flush();
var chipsArray = getChipsElements(element);
var chipArray = getChipElements(element);
// Check the lengths
expect(chipsArray.length).toBe(3);
expect(chipArray.length).toBe(3);
// Check the chip's text
expect(chipArray[0].innerHTML).toContain('1');
expect(chipArray[1].innerHTML).toContain('2');
expect(chipArray[2].innerHTML).toContain('3');
});
it('does not allow removal of chips', function() {
scope.chipItem = 'Football';
var element = buildChips(STATIC_CHIPS_TEMPLATE);
var wrap = element.find('md-chips-wrap');
expect(wrap).not.toHaveClass('md-removable');
});
});
describe('<md-chip-remove>', function() {
it('should remove a chip', function() {