UNPKG

angular-ui-bootstrap

Version:

Native AngularJS (Angular) directives for Bootstrap

1,254 lines (983 loc) 63.5 kB
describe('typeahead tests', function() { var $scope, $compile, $document, $templateCache, $timeout, $window; var changeInputValueTo; beforeEach(module('ui.bootstrap.typeahead')); beforeEach(module('ngSanitize')); beforeEach(module('uib/template/typeahead/typeahead-popup.html')); beforeEach(module('uib/template/typeahead/typeahead-match.html')); beforeEach(module(function($compileProvider) { $compileProvider.directive('formatter', function() { return { require: 'ngModel', link: function (scope, elm, attrs, ngModelCtrl) { ngModelCtrl.$formatters.unshift(function(viewVal) { return 'formatted' + viewVal; }); } }; }); $compileProvider.directive('childDirective', function() { return { restrict: 'A', require: '^parentDirective', link: function(scope, element, attrs, ctrl) {} }; }); })); beforeEach(inject(function(_$rootScope_, _$compile_, _$document_, _$templateCache_, _$timeout_, _$window_, $sniffer) { $scope = _$rootScope_; $scope.source = ['foo', 'bar', 'baz']; $scope.states = [ {code: 'AL', name: 'Alaska'}, {code: 'CL', name: 'California'} ]; $compile = _$compile_; $document = _$document_; $templateCache = _$templateCache_; $timeout = _$timeout_; $window = _$window_; changeInputValueTo = function(element, value) { var inputEl = findInput(element); inputEl.val(value); inputEl.trigger($sniffer.hasEvent('input') ? 'input' : 'change'); $scope.$digest(); }; })); //utility functions var prepareInputEl = function(inputTpl) { var el = $compile(angular.element(inputTpl))($scope); $scope.$digest(); return el; }; var findInput = function(element) { return element.find('input'); }; var findDropDown = function(element) { return element.find('ul.dropdown-menu'); }; var findMatches = function(element) { return findDropDown(element).find('li'); }; var triggerKeyDown = function(element, keyCode, options) { options = options || {}; var inputEl = findInput(element); var e = $.Event('keydown'); e.which = keyCode; if (options.shiftKey) { e.shiftKey = true; } inputEl.trigger(e); }; //custom matchers beforeEach(function () { jasmine.addMatchers({ toBeClosed: function(util, customEqualityTesters) { return { compare: function(actual, expected) { var typeaheadEl = findDropDown(actual); var result = { pass: util.equals(typeaheadEl.hasClass('ng-hide'), true, customEqualityTesters) }; if (result.pass) { result.message = 'Expected "' + angular.mock.dump(typeaheadEl) + '" not to be closed.'; } else { result.message = 'Expected "' + angular.mock.dump(typeaheadEl) + '" to be closed.'; } return result; } }; }, toBeOpenWithActive: function(util, customEqualityTesters) { return { compare: function(actual, noOfMatches, activeIdx) { var typeaheadEl = findDropDown(actual); var liEls = findMatches(actual); var result = { pass: util.equals(typeaheadEl.length, 1, customEqualityTesters) && util.equals(typeaheadEl.hasClass('ng-hide'), false, customEqualityTesters) && util.equals(liEls.length, noOfMatches, customEqualityTesters) && activeIdx === -1 ? !$(liEls).hasClass('active') : $(liEls[activeIdx]).hasClass('active') }; if (result.pass) { result.message = 'Expected "' + actual + '" not to be opened.'; } else { result.message = 'Expected "' + actual + '" to be opened.'; } return result; } }; } }); }); afterEach(function() { findDropDown($document.find('body')).remove(); }); //coarse grained, "integration" tests describe('initial state and model changes', function() { it('should be closed by default', function() { var element = prepareInputEl('<div><input ng-model="result" uib-typeahead="item for item in source"></div>'); expect(element).toBeClosed(); }); it('should correctly render initial state if the "as" keyword is used', function() { $scope.result = $scope.states[0]; var element = prepareInputEl('<div><input ng-model="result" uib-typeahead="state as state.name for state in states"></div>'); var inputEl = findInput(element); expect(inputEl.val()).toEqual('Alaska'); }); it('should default to bound model for initial rendering if there is not enough info to render label', function() { $scope.result = $scope.states[0].code; var element = prepareInputEl('<div><input ng-model="result" uib-typeahead="state.code as state.name + state.code for state in states"></div>'); var inputEl = findInput(element); expect(inputEl.val()).toEqual('AL'); }); it('should not get open on model change', function() { var element = prepareInputEl('<div><input ng-model="result" uib-typeahead="item for item in source"></div>'); $scope.$apply(function () { $scope.result = 'foo'; }); expect(element).toBeClosed(); }); }); describe('basic functionality', function() { it('should open and close typeahead based on matches', function() { var element = prepareInputEl('<div><input ng-model="result" uib-typeahead="item for item in source | filter:$viewValue"></div>'); var inputEl = findInput(element); var ownsId = inputEl.attr('aria-owns'); expect(inputEl.attr('aria-expanded')).toBe('false'); expect(inputEl.attr('aria-activedescendant')).toBeUndefined(); changeInputValueTo(element, 'ba'); expect(element).toBeOpenWithActive(2, 0); expect(findDropDown(element).attr('id')).toBe(ownsId); expect(inputEl.attr('aria-expanded')).toBe('true'); var activeOptionId = ownsId + '-option-0'; expect(inputEl.attr('aria-activedescendant')).toBe(activeOptionId); expect(findDropDown(element).find('li.active').attr('id')).toBe(activeOptionId); changeInputValueTo(element, ''); expect(element).toBeClosed(); expect(inputEl.attr('aria-expanded')).toBe('false'); expect(inputEl.attr('aria-activedescendant')).toBeUndefined(); }); it('should allow expressions over multiple lines', function() { var element = prepareInputEl('<div><input ng-model="result" uib-typeahead="item for item in source \n' + '| filter:$viewValue"></div>'); changeInputValueTo(element, 'ba'); expect(element).toBeOpenWithActive(2, 0); changeInputValueTo(element, ''); expect(element).toBeClosed(); }); it('should not open typeahead if input value smaller than a defined threshold', function() { var element = prepareInputEl('<div><input ng-model="result" uib-typeahead="item for item in source | filter:$viewValue" typeahead-min-length="2"></div>'); changeInputValueTo(element, 'b'); expect(element).toBeClosed(); }); it('should support changing min-length', function() { $scope.typeAheadMinLength = 2; var element = prepareInputEl('<div><input ng-model="result" uib-typeahead="item for item in source | filter:$viewValue" typeahead-min-length="typeAheadMinLength"></div>'); changeInputValueTo(element, 'b'); expect(element).toBeClosed(); $scope.typeAheadMinLength = 0; $scope.$digest(); changeInputValueTo(element, ''); expect(element).toBeOpenWithActive(3, 0); $scope.typeAheadMinLength = 2; $scope.$digest(); changeInputValueTo(element, 'b'); expect(element).toBeClosed(); }); it('should support custom model selecting function', function() { $scope.updaterFn = function(selectedItem) { return 'prefix' + selectedItem; }; var element = prepareInputEl('<div><input ng-model="result" uib-typeahead="updaterFn(item) as item for item in source | filter:$viewValue"></div>'); changeInputValueTo(element, 'f'); triggerKeyDown(element, 13); expect($scope.result).toEqual('prefixfoo'); }); it('should support custom label rendering function', function() { $scope.formatterFn = function(sourceItem) { return 'prefix' + sourceItem; }; var element = prepareInputEl('<div><input ng-model="result" uib-typeahead="item as formatterFn(item) for item in source | filter:$viewValue"></div>'); changeInputValueTo(element, 'fo'); var matchHighlight = findMatches(element).find('a').html(); expect(matchHighlight).toEqual('prefix<strong>fo</strong>o'); }); it('should by default bind view value to model even if not part of matches', function() { var element = prepareInputEl('<div><input ng-model="result" uib-typeahead="item for item in source | filter:$viewValue"></div>'); changeInputValueTo(element, 'not in matches'); expect($scope.result).toEqual('not in matches'); }); it('should support the editable property to limit model bindings to matches only', function() { var element = prepareInputEl('<div><input ng-model="result" uib-typeahead="item for item in source | filter:$viewValue" typeahead-editable="false"></div>'); changeInputValueTo(element, 'not in matches'); expect($scope.result).toEqual(undefined); }); it('should set validation errors for non-editable inputs', function() { var element = prepareInputEl( '<div><form name="form">' + '<input name="input" ng-model="result" uib-typeahead="item for item in source | filter:$viewValue" typeahead-editable="false">' + '</form></div>'); changeInputValueTo(element, 'not in matches'); expect($scope.result).toEqual(undefined); expect($scope.form.input.$error.editable).toBeTruthy(); changeInputValueTo(element, 'foo'); triggerKeyDown(element, 13); expect($scope.result).toEqual('foo'); expect($scope.form.input.$error.editable).toBeFalsy(); }); it('should not set editable validation error for empty input', function() { var element = prepareInputEl( '<div><form name="form">' + '<input name="input" ng-model="result" uib-typeahead="item for item in source | filter:$viewValue" typeahead-editable="false">' + '</form></div>'); changeInputValueTo(element, 'not in matches'); expect($scope.result).toEqual(undefined); expect($scope.form.input.$error.editable).toBeTruthy(); changeInputValueTo(element, ''); expect($scope.result).toEqual(null); expect($scope.form.input.$error.editable).toBeFalsy(); }); it('should clear view value after blur for typeahead-editable="false"', function () { var element = prepareInputEl('<div><input ng-model="result" uib-typeahead="item for item in source | filter:$viewValue" typeahead-editable="false"></div>'); var inputEl = findInput(element); changeInputValueTo(element, 'not in matches'); expect($scope.result).toEqual(undefined); expect(inputEl.val()).toEqual('not in matches'); inputEl.blur(); // input loses focus expect($scope.result).toEqual(undefined); expect(inputEl.val()).toEqual(''); }); it('should clear errors after blur for typeahead-editable="false"', function () { var element = prepareInputEl( '<div><form name="form">' + '<input name="input" ng-model="result" uib-typeahead="item for item in source | filter:$viewValue" typeahead-editable="false">' + '</form></div>'); var inputEl = findInput(element); changeInputValueTo(element, 'not in matches'); expect($scope.result).toEqual(undefined); expect(inputEl.val()).toEqual('not in matches'); inputEl.blur(); expect($scope.form.input.$error.editable).toBeFalsy(); expect($scope.form.input.$error.parse).toBeFalsy(); }); // fix for #6032 it('should clear errors and refresh scope after blur for typeahead-editable="false"', function () { var element = prepareInputEl( '<div><form name="form" ng-class="{invalid : form.input.$invalid}">' + '<input name="input" ng-model="result" uib-typeahead="item for item in source | filter:$viewValue" typeahead-editable="false">' + '</form></div>'); var inputEl = findInput(element); // first try changeInputValueTo(element, 'not in matches'); expect($scope.result).toEqual(undefined); expect(inputEl.val()).toEqual('not in matches'); expect(element.find('form')).toHaveClass('invalid'); inputEl.blur(); expect(inputEl.val()).toEqual(''); // <-- input is reset expect($scope.form.input.$error.editable).toBeFalsy(); expect($scope.form.input.$error.parse).toBeFalsy(); expect(element.find('form')).not.toHaveClass('invalid'); // <-- form has no error (it always works for some reason) // second try changeInputValueTo(element, 'not in matches'); expect($scope.result).toEqual(undefined); expect(inputEl.val()).toEqual('not in matches'); expect(element.find('form')).toHaveClass('invalid'); inputEl.blur(); expect(inputEl.val()).toEqual(''); // <-- input is reset expect($scope.form.input.$error.editable).toBeFalsy(); expect($scope.form.input.$error.parse).toBeFalsy(); expect(element.find('form')).not.toHaveClass('invalid'); // <-- form has no error (it didn't work prior to #6032 fix) }); it('should go through other validators after blur for typeahead-editable="false"', function () { var element = prepareInputEl( '<div><form name="form">' + '<input name="input" ng-model="result" uib-typeahead="item for item in source | filter:$viewValue" typeahead-editable="false" required>' + '</form></div>'); var inputEl = findInput(element); changeInputValueTo(element, 'not in matches'); expect($scope.result).toEqual(undefined); expect(inputEl.val()).toEqual('not in matches'); inputEl.blur(); // input loses focus expect($scope.result).toEqual(undefined); expect($scope.form.input.$error.required).toBeTruthy(); }); it('should clear view value when no value selected for typeahead-editable="false" typeahead-select-on-blur="false"', function () { var element = prepareInputEl('<div><input ng-model="result" uib-typeahead="item for item in source | filter:$viewValue" typeahead-editable="false" typeahead-select-on-blur="false"></div>'); var inputEl = findInput(element); changeInputValueTo(element, 'b'); expect($scope.result).toEqual(undefined); expect(inputEl.val()).toEqual('b'); inputEl.blur(); // input loses focus expect($scope.result).toEqual(undefined); expect(inputEl.val()).toEqual(''); }); it('should not clear view value when there is match but no value selected for typeahead-editable="false" typeahead-select-on-blur="true"', function () { var element = prepareInputEl('<div><input ng-model="result" uib-typeahead="item for item in source | filter:$viewValue" typeahead-editable="false" typeahead-select-on-blur="true"></div>'); var inputEl = findInput(element); changeInputValueTo(element, 'b'); expect($scope.result).toEqual(undefined); expect(inputEl.val()).toEqual('b'); inputEl.blur(); // input loses focus expect($scope.result).toEqual('bar'); expect(inputEl.val()).toEqual('bar'); }); it('should support changing the editable property to limit model bindings to matches only', function() { $scope.isEditable = true; var element = prepareInputEl('<div><input ng-model="result" uib-typeahead="item for item in source | filter:$viewValue" typeahead-editable="isEditable"></div>'); $scope.isEditable = false; $scope.$digest(); changeInputValueTo(element, 'not in matches'); expect($scope.result).toEqual(undefined); }); it('should support changing the editable property to bind view value to model even if not part of matches', function() { $scope.isEditable = false; var element = prepareInputEl('<div><input ng-model="result" uib-typeahead="item for item in source | filter:$viewValue" typeahead-editable="isEditable"></div>'); $scope.isEditable = true; $scope.$digest(); changeInputValueTo(element, 'not in matches'); expect($scope.result).toEqual('not in matches'); }); it('should bind loading indicator expression', inject(function($timeout) { $scope.isLoading = false; $scope.loadMatches = function(viewValue) { return $timeout(function() { return []; }, 1000); }; var element = prepareInputEl('<div><input ng-model="result" uib-typeahead="item for item in loadMatches()" typeahead-loading="isLoading"></div>'); changeInputValueTo(element, 'foo'); expect($scope.isLoading).toBeTruthy(); $timeout.flush(); expect($scope.isLoading).toBeFalsy(); })); it('should support timeout before trying to match $viewValue', inject(function($timeout) { var element = prepareInputEl('<div><input ng-model="result" uib-typeahead="item for item in source | filter:$viewValue" typeahead-wait-ms="200"></div>'); changeInputValueTo(element, 'foo'); expect(element).toBeClosed(); $timeout.flush(); expect(element).toBeOpenWithActive(1, 0); })); it('should cancel old timeouts when something is typed within waitTime', inject(function($timeout) { var values = []; $scope.loadMatches = function(viewValue) { values.push(viewValue); return $scope.source; }; var element = prepareInputEl('<div><input ng-model="result" uib-typeahead="item for item in loadMatches($viewValue) | filter:$viewValue" typeahead-wait-ms="200"></div>'); changeInputValueTo(element, 'first'); changeInputValueTo(element, 'second'); $timeout.flush(); expect(values).not.toContain('first'); })); it('should allow timeouts when something is typed after waitTime has passed', inject(function($timeout) { var values = []; $scope.loadMatches = function(viewValue) { values.push(viewValue); return $scope.source; }; var element = prepareInputEl('<div><input ng-model="result" uib-typeahead="item for item in loadMatches($viewValue) | filter:$viewValue" typeahead-wait-ms="200"></div>'); changeInputValueTo(element, 'first'); $timeout.flush(); expect(values).toContain('first'); changeInputValueTo(element, 'second'); $timeout.flush(); expect(values).toContain('second'); })); it('should support custom popup templates', function() { $templateCache.put('custom.html', '<div class="custom">foo</div>'); var element = prepareInputEl('<div><input ng-model="result" typeahead-popup-template-url="custom.html" uib-typeahead="state as state.name for state in states | filter:$viewValue"></div>'); changeInputValueTo(element, 'Al'); expect(element.find('.custom').text()).toBe('foo'); }); it('should support custom templates for matched items', function() { $templateCache.put('custom.html', '<p>{{ index }} {{ match.label }}</p>'); var element = prepareInputEl('<div><input ng-model="result" typeahead-template-url="custom.html" uib-typeahead="state as state.name for state in states | filter:$viewValue"></div>'); changeInputValueTo(element, 'Al'); expect(findMatches(element).eq(0).find('p').text()).toEqual('0 Alaska'); }); it('should support directives which require controllers in custom templates for matched items', function() { $templateCache.put('custom.html', '<p child-directive>{{ index }} {{ match.label }}</p>'); var element = prepareInputEl('<div><input ng-model="result" typeahead-template-url="custom.html" uib-typeahead="state as state.name for state in states | filter:$viewValue"></div>'); element.data('$parentDirectiveController', {}); changeInputValueTo(element, 'Al'); expect(findMatches(element).eq(0).find('p').text()).toEqual('0 Alaska'); }); it('should throw error on invalid expression', function() { var prepareInvalidDir = function() { prepareInputEl('<div><input ng-model="result" uib-typeahead="an invalid expression"></div>'); }; expect(prepareInvalidDir).toThrow(); }); it('should remove the id attribute from the original DOM element', function() { var element = prepareInputEl('<div><input id="typeahead-element" ng-model="result" uib-typeahead="item for item in source | filter:$viewValue" typeahead-show-hint="true"></div>'); var inputEl = findInput(element); expect(inputEl.size()).toBe(2); expect(inputEl.eq(0).attr('id')).toBe(undefined); expect(inputEl.eq(1).attr('id')).toBe('typeahead-element'); }); }); describe('shouldSelect', function() { it('should select a match when function returns true', function() { $scope.shouldSelectFn = function() { return true; }; var element = prepareInputEl('<div><input ng-model="result" typeahead-should-select="shouldSelectFn($event)" uib-typeahead="item for item in source | filter:$viewValue"></div>'); var inputEl = findInput(element); changeInputValueTo(element, 'b'); triggerKeyDown(element, 13); expect($scope.result).toEqual('bar'); expect(inputEl.val()).toEqual('bar'); expect(element).toBeClosed(); }); it('should not select a match when function returns false', function() { $scope.shouldSelectFn = function() { return false; }; var element = prepareInputEl('<div><input ng-model="result" typeahead-should-select="shouldSelectFn($event)" uib-typeahead="item for item in source | filter:$viewValue"></div>'); var inputEl = findInput(element); changeInputValueTo(element, 'b'); triggerKeyDown(element, 13); // no change expect($scope.result).toEqual('b'); expect(inputEl.val()).toEqual('b'); }); it('should pass key event into select trigger function', function() { $scope.shouldSelectFn = jasmine.createSpy('shouldSelectFn');//.and.returnValue(true); var element = prepareInputEl('<div><input ng-model="result" typeahead-should-select="shouldSelectFn($event)" uib-typeahead="item for item in source | filter:$viewValue"></div>'); var inputEl = findInput(element); changeInputValueTo(element, 'b'); triggerKeyDown(element, 13); expect($scope.shouldSelectFn.calls.count()).toEqual(1); expect($scope.shouldSelectFn.calls.argsFor(0)[0].which).toEqual(13); }); }); describe('selecting a match', function() { it('should select a match on enter', function() { var element = prepareInputEl('<div><input ng-model="result" uib-typeahead="item for item in source | filter:$viewValue"></div>'); var inputEl = findInput(element); changeInputValueTo(element, 'b'); triggerKeyDown(element, 13); expect($scope.result).toEqual('bar'); expect(inputEl.val()).toEqual('bar'); expect(element).toBeClosed(); }); it('should select a match on tab', function() { var element = prepareInputEl('<div><input ng-model="result" uib-typeahead="item for item in source | filter:$viewValue"></div>'); var inputEl = findInput(element); changeInputValueTo(element, 'b'); triggerKeyDown(element, 9); expect($scope.result).toEqual('bar'); expect(inputEl.val()).toEqual('bar'); expect(element).toBeClosed(); }); it('should not select any match on blur without \'select-on-blur=true\' option', function() { var element = prepareInputEl('<div><input ng-model="result" uib-typeahead="item for item in source | filter:$viewValue"></div>'); var inputEl = findInput(element); changeInputValueTo(element, 'b'); inputEl.blur(); // input loses focus // no change expect($scope.result).toEqual('b'); expect(inputEl.val()).toEqual('b'); }); it('should select a match on blur with \'select-on-blur=true\' option', function() { var element = prepareInputEl('<div><input ng-model="result" uib-typeahead="item for item in source | filter:$viewValue" typeahead-select-on-blur="true"></div>'); var inputEl = findInput(element); changeInputValueTo(element, 'b'); inputEl.blur(); // input loses focus // first element should be selected expect($scope.result).toEqual('bar'); expect(inputEl.val()).toEqual('bar'); }); it('should select match on click', function() { var element = prepareInputEl('<div><input ng-model="result" uib-typeahead="item for item in source | filter:$viewValue"></div>'); var inputEl = findInput(element); changeInputValueTo(element, 'b'); var match = $(findMatches(element)[1]).find('a')[0]; $(match).click(); $scope.$digest(); expect($scope.result).toEqual('baz'); expect(inputEl.val()).toEqual('baz'); expect(element).toBeClosed(); }); it('should invoke select callback on select', function() { $scope.onSelect = function($item, $model, $label, $event) { $scope.$item = $item; $scope.$model = $model; $scope.$label = $label; $scope.$event = $event; }; var element = prepareInputEl('<div><input ng-model="result" typeahead-on-select="onSelect($item, $model, $label, $event)" uib-typeahead="state.code as state.name for state in states | filter:$viewValue"></div>'); changeInputValueTo(element, 'Alas'); triggerKeyDown(element, 13); expect($scope.result).toEqual('AL'); expect($scope.$item).toEqual($scope.states[0]); expect($scope.$model).toEqual('AL'); expect($scope.$label).toEqual('Alaska'); expect($scope.$event.type).toEqual("keydown"); }); it('should correctly update inputs value on mapping where label is not derived from the model', function() { var element = prepareInputEl('<div><input ng-model="result" uib-typeahead="state.code as state.name for state in states | filter:$viewValue"></div>'); var inputEl = findInput(element); changeInputValueTo(element, 'Alas'); triggerKeyDown(element, 13); expect($scope.result).toEqual('AL'); expect(inputEl.val()).toEqual('AL'); }); it('should bind no results indicator as true when no matches returned', inject(function($timeout) { $scope.isNoResults = false; $scope.loadMatches = function(viewValue) { return $timeout(function() { return []; }, 1000); }; var element = prepareInputEl('<div><input ng-model="result" uib-typeahead="item for item in loadMatches()" typeahead-no-results="isNoResults"></div>'); changeInputValueTo(element, 'foo'); expect($scope.isNoResults).toBeFalsy(); $timeout.flush(); expect($scope.isNoResults).toBeTruthy(); })); it('should bind no results indicator as false when matches are returned', inject(function($timeout) { $scope.isNoResults = false; $scope.loadMatches = function(viewValue) { return $timeout(function() { return [viewValue]; }, 1000); }; var element = prepareInputEl('<div><input ng-model="result" uib-typeahead="item for item in loadMatches()" typeahead-no-results="isNoResults"></div>'); changeInputValueTo(element, 'foo'); expect($scope.isNoResults).toBeFalsy(); $timeout.flush(); expect($scope.isNoResults).toBeFalsy(); })); it('should not focus the input if `typeahead-focus-on-select` is false', function() { var element = prepareInputEl('<div><input ng-model="result" uib-typeahead="item for item in source | filter:$viewValue" typeahead-focus-on-select="false"></div>'); $document.find('body').append(element); var inputEl = findInput(element); changeInputValueTo(element, 'b'); var match = $(findMatches(element)[1]).find('a')[0]; $(match).click(); $scope.$digest(); $timeout.flush(); expect(document.activeElement).not.toBe(inputEl[0]); expect($scope.result).toEqual('baz'); }); }); describe('select on exact match', function() { it('should select on an exact match when set', function() { $scope.onSelect = jasmine.createSpy('onSelect'); var element = prepareInputEl('<div><input ng-model="result" typeahead-editable="false" typeahead-on-select="onSelect()" uib-typeahead="item for item in source | filter:$viewValue" typeahead-select-on-exact="true"></div>'); var inputEl = findInput(element); changeInputValueTo(element, 'bar'); expect($scope.result).toEqual('bar'); expect(inputEl.val()).toEqual('bar'); expect(element).toBeClosed(); expect($scope.onSelect).toHaveBeenCalled(); }); it('should not select on an exact match by default', function() { $scope.onSelect = jasmine.createSpy('onSelect'); var element = prepareInputEl('<div><input ng-model="result" typeahead-editable="false" typeahead-on-select="onSelect()" uib-typeahead="item for item in source | filter:$viewValue"></div>'); var inputEl = findInput(element); changeInputValueTo(element, 'bar'); expect($scope.result).toBeUndefined(); expect(inputEl.val()).toEqual('bar'); expect($scope.onSelect.calls.any()).toBe(false); }); it('should not be case sensitive when select on an exact match', function() { $scope.onSelect = jasmine.createSpy('onSelect'); var element = prepareInputEl('<div><input ng-model="result" typeahead-editable="false" typeahead-on-select="onSelect()" uib-typeahead="item for item in source | filter:$viewValue" typeahead-select-on-exact="true"></div>'); var inputEl = findInput(element); changeInputValueTo(element, 'BaR'); expect($scope.result).toEqual('bar'); expect(inputEl.val()).toEqual('bar'); expect(element).toBeClosed(); expect($scope.onSelect).toHaveBeenCalled(); }); it('should not auto select when not a match with one potential result left', function() { $scope.onSelect = jasmine.createSpy('onSelect'); var element = prepareInputEl('<div><input ng-model="result" typeahead-editable="false" typeahead-on-select="onSelect()" uib-typeahead="item for item in source | filter:$viewValue" typeahead-select-on-exact="true"></div>'); var inputEl = findInput(element); changeInputValueTo(element, 'fo'); expect($scope.result).toBeUndefined(); expect(inputEl.val()).toEqual('fo'); expect($scope.onSelect.calls.any()).toBe(false); }); }); describe('is-open indicator', function () { var element; beforeEach(function () { element = prepareInputEl('<div><input ng-model="result" uib-typeahead="item for item in source | filter:$viewValue" typeahead-is-open="isOpen"></div>'); }); it('should bind is-open indicator as true when matches are returned', function () { expect($scope.isOpen).toBeFalsy(); changeInputValueTo(element, 'b'); expect($scope.isOpen).toBeTruthy(); }); it('should bind is-open indicator as false when no matches returned', function () { expect($scope.isOpen).toBeFalsy(); changeInputValueTo(element, 'b'); expect($scope.isOpen).toBeTruthy(); changeInputValueTo(element, 'not match'); expect($scope.isOpen).toBeFalsy(); }); it('should bind is-open indicator as false when a match is clicked', function () { expect($scope.isOpen).toBeFalsy(); changeInputValueTo(element, 'b'); expect($scope.isOpen).toBeTruthy(); var match = findMatches(element).find('a').eq(0); match.click(); $scope.$digest(); expect($scope.isOpen).toBeFalsy(); }); it('should bind is-open indicator as false when click outside', function () { expect($scope.isOpen).toBeFalsy(); changeInputValueTo(element, 'b'); expect($scope.isOpen).toBeTruthy(); $document.find('body').click(); $scope.$digest(); expect($scope.isOpen).toBeFalsy(); }); it('should bind is-open indicator as false on enter', function () { expect($scope.isOpen).toBeFalsy(); changeInputValueTo(element, 'b'); expect($scope.isOpen).toBeTruthy(); triggerKeyDown(element, 13); expect($scope.isOpen).toBeFalsy(); }); it('should bind is-open indicator as false on tab', function () { expect($scope.isOpen).toBeFalsy(); changeInputValueTo(element, 'b'); expect($scope.isOpen).toBeTruthy(); triggerKeyDown(element, 9); expect($scope.isOpen).toBeFalsy(); }); it('should bind is-open indicator as false on escape key', function () { expect($scope.isOpen).toBeFalsy(); changeInputValueTo(element, 'b'); expect($scope.isOpen).toBeTruthy(); triggerKeyDown(element, 27); expect($scope.isOpen).toBeFalsy(); }); it('should bind is-open indicator as false input value smaller than a defined threshold', function () { var element = prepareInputEl('<div><input ng-model="result" uib-typeahead="item for item in source | filter:$viewValue" typeahead-is-open="isToggled" typeahead-min-length="2"></div>'); expect($scope.isToggled).toBeFalsy(); changeInputValueTo(element, 'b'); expect($scope.isToggled).toBeFalsy(); }); }); describe('pop-up interaction', function() { var element; beforeEach(function() { element = prepareInputEl('<div><input ng-model="result" uib-typeahead="item for item in source | filter:$viewValue"></div>'); }); it('should activate prev/next matches on up/down keys', function() { changeInputValueTo(element, 'b'); var parentNode = element.find('ul').eq(0)[0]; var liIndex; liIndex = 0; expect(element).toBeOpenWithActive(2, liIndex); expect(parentNode.scrollTop).toEqual(element.find('li').eq(liIndex)[0].offsetTop); // Down arrow key triggerKeyDown(element, 40); liIndex = 1; expect(element).toBeOpenWithActive(2, liIndex); expect(parentNode.scrollTop).toEqual(element.find('li').eq(liIndex)[0].offsetTop); // Down arrow key goes back to first element triggerKeyDown(element, 40); liIndex = 0; expect(element).toBeOpenWithActive(2, liIndex); expect(parentNode.scrollTop).toEqual(element.find('li').eq(liIndex)[0].offsetTop); // Up arrow key goes back to last element triggerKeyDown(element, 38); liIndex = 1; expect(element).toBeOpenWithActive(2, liIndex); expect(parentNode.scrollTop).toEqual(element.find('li').eq(liIndex)[0].offsetTop); // Up arrow key goes back to first element triggerKeyDown(element, 38); liIndex = 0; expect(parentNode.scrollTop).toEqual(element.find('li').eq(liIndex)[0].offsetTop); expect(element).toBeOpenWithActive(2, liIndex); }); it('should close popup on escape key', function() { changeInputValueTo(element, 'b'); expect(element).toBeOpenWithActive(2, 0); // Escape key triggerKeyDown(element, 27); expect(element).toBeClosed(); }); it('should highlight match on mouseenter', function() { changeInputValueTo(element, 'b'); expect(element).toBeOpenWithActive(2, 0); findMatches(element).eq(1).trigger('mouseenter'); expect(element).toBeOpenWithActive(2, 1); }); }); describe('promises', function() { var element, deferred; beforeEach(inject(function($q) { deferred = $q.defer(); $scope.source = function() { return deferred.promise; }; element = prepareInputEl('<div><input ng-model="result" uib-typeahead="item for item in source()"></div>'); })); it('should display matches from promise', function() { changeInputValueTo(element, 'c'); expect(element).toBeClosed(); deferred.resolve(['good', 'stuff']); $scope.$digest(); expect(element).toBeOpenWithActive(2, 0); }); it('should not display anything when promise is rejected', function() { changeInputValueTo(element, 'c'); expect(element).toBeClosed(); deferred.reject('fail'); $scope.$digest(); expect(element).toBeClosed(); }); it('PR #3178, resolves #2999 - should not return property "length" of undefined for undefined matches', function() { changeInputValueTo(element, 'c'); expect(element).toBeClosed(); deferred.resolve(); $scope.$digest(); expect(element).toBeClosed(); }); }); describe('non-regressions tests', function() { it('issue 231 - closes matches popup on click outside typeahead', function() { var element = prepareInputEl('<div><input ng-model="result" uib-typeahead="item for item in source | filter:$viewValue"></div>'); changeInputValueTo(element, 'b'); $document.find('body').click(); $scope.$digest(); expect(element).toBeClosed(); }); it('issue 591 - initial formatting for un-selected match and complex label expression', function() { var inputEl = findInput(prepareInputEl('<div><input ng-model="result" uib-typeahead="state as state.name + \' \' + state.code for state in states | filter:$viewValue"></div>')); expect(inputEl.val()).toEqual(''); }); it('issue 786 - name of internal model should not conflict with scope model name', function() { $scope.state = $scope.states[0]; var element = prepareInputEl('<div><input ng-model="state" uib-typeahead="state as state.name for state in states | filter:$viewValue"></div>'); var inputEl = findInput(element); expect(inputEl.val()).toEqual('Alaska'); }); it('issue 863 - it should work correctly with input type="email"', function() { $scope.emails = ['foo@host.com', 'bar@host.com']; var element = prepareInputEl('<div><input type="email" ng-model="email" uib-typeahead="email for email in emails | filter:$viewValue"></div>'); var inputEl = findInput(element); changeInputValueTo(element, 'bar'); expect(element).toBeOpenWithActive(1, 0); triggerKeyDown(element, 13); expect($scope.email).toEqual('bar@host.com'); expect(inputEl.val()).toEqual('bar@host.com'); }); it('issue 964 - should not show popup with matches if an element is not focused', function() { $scope.items = function(viewValue) { return $timeout(function() { return [viewValue]; }); }; var element = prepareInputEl('<div><input ng-model="result" uib-typeahead="item for item in items($viewValue)"></div>'); var inputEl = findInput(element); changeInputValueTo(element, 'match'); $scope.$digest(); inputEl.blur(); $timeout.flush(); expect(element).toBeClosed(); }); it('should properly update loading callback if an element is not focused', function() { $scope.items = function(viewValue) { return $timeout(function(){ return [viewValue]; }); }; var element = prepareInputEl('<div><input ng-model="result" typeahead-loading="isLoading" uib-typeahead="item for item in items($viewValue)"></div>'); var inputEl = findInput(element); changeInputValueTo(element, 'match'); $scope.$digest(); inputEl.blur(); $timeout.flush(); expect($scope.isLoading).toBeFalsy(); }); it('issue 1140 - should properly update loading callback when deleting characters', function() { $scope.items = function(viewValue) { return $timeout(function() { return [viewValue]; }); }; var element = prepareInputEl('<div><input ng-model="result" typeahead-min-length="2" typeahead-loading="isLoading" uib-typeahead="item for item in items($viewValue)"></div>'); changeInputValueTo(element, 'match'); $scope.$digest(); expect($scope.isLoading).toBeTruthy(); changeInputValueTo(element, 'm'); $timeout.flush(); $scope.$digest(); expect($scope.isLoading).toBeFalsy(); }); it('should cancel old timeout when deleting characters', inject(function($timeout) { var values = []; $scope.loadMatches = function(viewValue) { values.push(viewValue); return $scope.source; }; var element = prepareInputEl('<div><input ng-model="result" uib-typeahead="item for item in loadMatches($viewValue) | filter:$viewValue" typeahead-min-length="2" typeahead-wait-ms="200"></div>'); changeInputValueTo(element, 'match'); changeInputValueTo(element, 'm'); $timeout.flush(); expect(values).not.toContain('match'); })); describe('', function() { // Dummy describe to be able to create an after hook for this tests var element; it('does not close matches popup on click in input', function() { element = prepareInputEl('<div><input ng-model="result" uib-typeahead="item for item in source | filter:$viewValue"></div>'); var inputEl = findInput(element); // Note that this bug can only be found when element is in the document $document.find('body').append(element); changeInputValueTo(element, 'b'); inputEl.click(); $scope.$digest(); expect(element).toBeOpenWithActive(2, 0); }); it('issue #1773 - should not trigger an error when used with ng-focus', function() { element = prepareInputEl('<div><input ng-model="result" uib-typeahead="item for item in source | filter:$viewValue" ng-focus="foo()"></div>'); var inputEl = findInput(element); // Note that this bug can only be found when element is in the document $document.find('body').append(element); changeInputValueTo(element, 'b'); var match = $(findMatches(element)[1]).find('a')[0]; $(match).click(); $scope.$digest(); }); afterEach(function() { element.remove(); }); }); it('issue #1238 - allow names like "query" to be used inside "in" expressions ', function() { $scope.query = function() { return ['foo', 'bar']; }; var element = prepareInputEl('<div><input ng-model="result" uib-typeahead="item for item in query($viewValue)"></div>'); changeInputValueTo(element, 'bar'); expect(element).toBeOpenWithActive(2, 0); }); it('issue #3318 - should set model validity to true when set manually', function() { var element = prepareInputEl( '<div><form name="form">' + '<input name="input" ng-model="result" uib-typeahead="item for item in source | filter:$viewValue" typeahead-editable="false">' + '</form></div>'); changeInputValueTo(element, 'not in matches'); $scope.$apply(function() { $scope.result = 'manually set'; }); expect($scope.result).toEqual('manually set'); expect($scope.form.input.$valid).toBeTruthy(); }); it('issue #3166 - should set \'parse\' key as valid when selecting a perfect match and not editable', function() { var element = prepareInputEl('<div ng-form="test"><input name="typeahead" ng-model="result" uib-typeahead="state as state.name for state in states | filter:$viewValue" typeahead-editable="false"></div>'); var inputEl = findInput(element); changeInputValueTo(element, 'Alaska'); triggerKeyDown(element, 13); expect($scope.test.typeahead.$error.parse).toBeUndefined(); }); }); describe('ng-model-options', function() { it('should support getterSetter', function() { function resultSetter(state) { return state; } $scope.result = resultSetter; var element = prepareInputEl('<div><input name="typeahead" ng-model="result" ng-model-options="{getterSetter: true}" uib-typeahead="state as state.name for state in states | filter:$viewValue" typeahead-editable="false"></div>'); changeInputValueTo(element, 'Alaska'); triggerKeyDown(element, 13); expect($scope.result).toBe(resultSetter); }); describe('debounce as a number', function() { it('should work with selecting via keyboard', function() { element = prepareInputEl('<div><input name="typeahead" ng-model="result" ng-model-options="{debounce: 400}" uib-typeahead="state as state.name for state in states | filter:$viewvalue"></div>'); var inputEl = findInput(element); changeInputValueTo(element, 'Alaska'); triggerKeyDown(element, 13); expect($scope.result).not.toBe('Alaska'); $timeout.flush(400); expect($scope.result).toBe('Alaska'); }); it('should work with select on exact', function() { element = prepareInputEl('<div><input name="typeahead" ng-model="result" ng-model-options="{debounce: 400}" uib-typeahead="state as state.name for state in states | filter:$viewvalue" typeahead-select-on-exact="true"></div>'); var inputEl = findInput(element); changeInputValueTo(element, 'Alaska'); expect($scope.result).not.toBe('Alaska'); $timeout.flush(400); expect($scope.result).toBe('Alaska'); }); it('should work with selecting a match via click', function() { element = prepareInputEl('<div><input name="typeahead" ng-model="result" ng-model-options="{debounce: 400}" uib-typeahead="state as state.name for state in states | filter:$viewvalue"></div>'); var inputEl = findInput(element); changeInputValueTo(element, 'Alaska'); var match = $(findMatches(element)[0]).find('a')[0]; $(match).click(); $scope.$digest(); expect($scope.result).not.toBe('Alaska'); $timeout.flush(400); expect($scope.result).toBe('Alaska'); }); }); describe('debounce as an object', function() { it('should work with selecting via keyboard', function() { element = prepareInputEl('<div><input name="typeahead" ng-model="result" ng-model-options="{debounce: {default: 400, blur: 500}}" uib-typeahead="state as state.name for state in states | filter:$viewvalue"></div>'); var inputEl = findInput(element); changeInputValueTo(element, 'Alaska'); triggerKeyDown(element, 13); expect($scope.result).not.toBe('Alaska'); $timeout.flush(400); expect($scope.result).toBe('Alaska'); }); it('should work with select on exact', function() { element = prepareInputEl('<div><input name="typeahead" ng-model="result" ng-model-options="{debounce: {default: 400, blur: 500}}" uib-typeahead="state as state.name for state in states | filter:$viewvalue" typeahead-select-on-exact="true"></div>'); var inputEl = findInput(element); changeInputValueTo(element, 'Alaska'); expect($scope.result).not.toBe('Alaska'); $timeout.flush(400); expect($scope.result).toBe('Alaska'); }); it('should work with selecting a match via click', function() { element = prepareInputEl('<div><input name="typeahead" ng-model="result" ng-model-options="{debounce: {default: 400, blur: 500}}" uib-typeahead="state as state.name for state in states | filter:$viewvalue"></div>'); var inputEl = findInput(element); changeInputValueTo(element, 'Alaska'); var match = $(findMatches(element)[0]).find('a')[0]; $(match).click(); $scope.$digest(); expect($scope.result).not.toBe('Alaska'); $timeout.flush(400); expect($scope.result).toBe('Alaska'); }); it('should work when blurring and select on blur', function() { element = prepareInputEl('<div><input name="typeahead" ng-model="result" ng-model-options="{debounce: {default: 400, blur: 500}}" uib-typeahead="state as state.name for state in states | filter:$viewvalue" typeahead-select-on-blur="true"></div>'); var inputEl = findInput(element); changeInputValueTo(element, 'Alaska'); element.blur(); $scope.$digest(); expect($scope.result).not.toBe('Alaska'); $timeout.flush(500); expect($scope.result).toBe('Alaska'); }); }); }); describe('input formatting', function() { it('should co-operate with existing formatters', function() { $scope.result = $scope.states[0]; var element = prepareInputEl('<div><input ng-model="result.name" formatter uib-typeahead="state.name for state in states | filter:$viewValue"></div>'), inputEl = findInput(element); expect(inputEl.val()).toEqual('formatted' + $scope.result.name); }); it('should support a custom input formatting function', function() { $scope.result = $scope.states[0]; $scope.formatInput = function($model) { return $model.code; }; var element = prepareInputEl('<div><input ng-model="result" typeahead-input-formatter="formatInput($model)" uib-typeahead="state as state.name for state in states | filter:$viewValue"></div>'), inputEl = findInput(element); expect(inputEl.val()).toEqual('AL'); expect($scope.result).toEqual($scope.states[0]); }); }); describe('i