blob: 040a2289e8d73019d6d9eb779a8910edcca9d644 [file] [log] [blame]
Matteo Scandolo46b56102015-12-16 14:23:08 -08001describe('typeahead tests', function() {
2 var $scope, $compile, $document, $templateCache, $timeout, $window;
3 var changeInputValueTo;
4
5 beforeEach(module('ui.bootstrap.typeahead'));
6 beforeEach(module('ngSanitize'));
7 beforeEach(module('template/typeahead/typeahead-popup.html'));
8 beforeEach(module('template/typeahead/typeahead-match.html'));
9 beforeEach(module(function($compileProvider) {
10 $compileProvider.directive('formatter', function() {
11 return {
12 require: 'ngModel',
13 link: function (scope, elm, attrs, ngModelCtrl) {
14 ngModelCtrl.$formatters.unshift(function(viewVal) {
15 return 'formatted' + viewVal;
16 });
17 }
18 };
19 });
20 $compileProvider.directive('childDirective', function() {
21 return {
22 restrict: 'A',
23 require: '^parentDirective',
24 link: function(scope, element, attrs, ctrl) {}
25 };
26 });
27 }));
28 beforeEach(inject(function(_$rootScope_, _$compile_, _$document_, _$templateCache_, _$timeout_, _$window_, $sniffer) {
29 $scope = _$rootScope_;
30 $scope.source = ['foo', 'bar', 'baz'];
31 $scope.states = [
32 {code: 'AL', name: 'Alaska'},
33 {code: 'CL', name: 'California'}
34 ];
35 $compile = _$compile_;
36 $document = _$document_;
37 $templateCache = _$templateCache_;
38 $timeout = _$timeout_;
39 $window = _$window_;
40 changeInputValueTo = function(element, value) {
41 var inputEl = findInput(element);
42 inputEl.val(value);
43 inputEl.trigger($sniffer.hasEvent('input') ? 'input' : 'change');
44 $scope.$digest();
45 };
46 }));
47
48 //utility functions
49 var prepareInputEl = function(inputTpl) {
50 var el = $compile(angular.element(inputTpl))($scope);
51 $scope.$digest();
52 return el;
53 };
54
55 var findInput = function(element) {
56 return element.find('input');
57 };
58
59 var findDropDown = function(element) {
60 return element.find('ul.dropdown-menu');
61 };
62
63 var findMatches = function(element) {
64 return findDropDown(element).find('li');
65 };
66
67 var triggerKeyDown = function(element, keyCode) {
68 var inputEl = findInput(element);
69 var e = $.Event('keydown');
70 e.which = keyCode;
71 inputEl.trigger(e);
72 };
73
74 //custom matchers
75 beforeEach(function () {
76 jasmine.addMatchers({
77 toBeClosed: function(util, customEqualityTesters) {
78 return {
79 compare: function(actual, expected) {
80 var typeaheadEl = findDropDown(actual);
81
82 var result = {
83 pass: util.equals(typeaheadEl.hasClass('ng-hide'), true, customEqualityTesters)
84 };
85
86 if (result.pass) {
87 result.message = 'Expected "' + angular.mock.dump(typeaheadEl) + '" not to be closed.';
88 } else {
89 result.message = 'Expected "' + angular.mock.dump(typeaheadEl) + '" to be closed.';
90 }
91
92 return result;
93 }
94 };
95 },
96 toBeOpenWithActive: function(util, customEqualityTesters) {
97 return {
98 compare: function(actual, noOfMatches, activeIdx) {
99 var typeaheadEl = findDropDown(actual);
100 var liEls = findMatches(actual);
101
102 var result = {
103 pass: util.equals(typeaheadEl.length, 1, customEqualityTesters) &&
104 util.equals(typeaheadEl.hasClass('ng-hide'), false, customEqualityTesters) &&
105 util.equals(liEls.length, noOfMatches, customEqualityTesters) &&
106 activeIdx === -1 ? !$(liEls).hasClass('active') : $(liEls[activeIdx]).hasClass('active')
107 };
108
109 if (result.pass) {
110 result.message = 'Expected "' + actual + '" not to be opened.';
111 } else {
112 result.message = 'Expected "' + actual + '" to be opened.';
113 }
114
115 return result;
116 }
117 };
118 }
119 });
120 });
121
122 afterEach(function() {
123 findDropDown($document.find('body')).remove();
124 });
125
126 //coarse grained, "integration" tests
127 describe('initial state and model changes', function() {
128 it('should be closed by default', function() {
129 var element = prepareInputEl('<div><input ng-model="result" uib-typeahead="item for item in source"></div>');
130 expect(element).toBeClosed();
131 });
132
133 it('should correctly render initial state if the "as" keyword is used', function() {
134 $scope.result = $scope.states[0];
135
136 var element = prepareInputEl('<div><input ng-model="result" uib-typeahead="state as state.name for state in states"></div>');
137 var inputEl = findInput(element);
138
139 expect(inputEl.val()).toEqual('Alaska');
140 });
141
142 it('should default to bound model for initial rendering if there is not enough info to render label', function() {
143 $scope.result = $scope.states[0].code;
144
145 var element = prepareInputEl('<div><input ng-model="result" uib-typeahead="state.code as state.name + state.code for state in states"></div>');
146 var inputEl = findInput(element);
147
148 expect(inputEl.val()).toEqual('AL');
149 });
150
151 it('should not get open on model change', function() {
152 var element = prepareInputEl('<div><input ng-model="result" uib-typeahead="item for item in source"></div>');
153 $scope.$apply(function () {
154 $scope.result = 'foo';
155 });
156 expect(element).toBeClosed();
157 });
158 });
159
160 describe('basic functionality', function() {
161 it('should open and close typeahead based on matches', function() {
162 var element = prepareInputEl('<div><input ng-model="result" uib-typeahead="item for item in source | filter:$viewValue"></div>');
163 var inputEl = findInput(element);
164 var ownsId = inputEl.attr('aria-owns');
165
166 expect(inputEl.attr('aria-expanded')).toBe('false');
167 expect(inputEl.attr('aria-activedescendant')).toBeUndefined();
168
169 changeInputValueTo(element, 'ba');
170 expect(element).toBeOpenWithActive(2, 0);
171 expect(findDropDown(element).attr('id')).toBe(ownsId);
172 expect(inputEl.attr('aria-expanded')).toBe('true');
173 var activeOptionId = ownsId + '-option-0';
174 expect(inputEl.attr('aria-activedescendant')).toBe(activeOptionId);
175 expect(findDropDown(element).find('li.active').attr('id')).toBe(activeOptionId);
176
177 changeInputValueTo(element, '');
178 expect(element).toBeClosed();
179 expect(inputEl.attr('aria-expanded')).toBe('false');
180 expect(inputEl.attr('aria-activedescendant')).toBeUndefined();
181 });
182
183 it('should allow expressions over multiple lines', function() {
184 var element = prepareInputEl('<div><input ng-model="result" uib-typeahead="item for item in source \n' +
185 '| filter:$viewValue"></div>');
186 changeInputValueTo(element, 'ba');
187 expect(element).toBeOpenWithActive(2, 0);
188
189 changeInputValueTo(element, '');
190 expect(element).toBeClosed();
191 });
192
193 it('should not open typeahead if input value smaller than a defined threshold', function() {
194 var element = prepareInputEl('<div><input ng-model="result" uib-typeahead="item for item in source | filter:$viewValue" typeahead-min-length="2"></div>');
195 changeInputValueTo(element, 'b');
196 expect(element).toBeClosed();
197 });
198
199 it('should support custom model selecting function', function() {
200 $scope.updaterFn = function(selectedItem) {
201 return 'prefix' + selectedItem;
202 };
203 var element = prepareInputEl('<div><input ng-model="result" uib-typeahead="updaterFn(item) as item for item in source | filter:$viewValue"></div>');
204 changeInputValueTo(element, 'f');
205 triggerKeyDown(element, 13);
206 expect($scope.result).toEqual('prefixfoo');
207 });
208
209 it('should support custom label rendering function', function() {
210 $scope.formatterFn = function(sourceItem) {
211 return 'prefix' + sourceItem;
212 };
213
214 var element = prepareInputEl('<div><input ng-model="result" uib-typeahead="item as formatterFn(item) for item in source | filter:$viewValue"></div>');
215 changeInputValueTo(element, 'fo');
216 var matchHighlight = findMatches(element).find('a').html();
217 expect(matchHighlight).toEqual('prefix<strong>fo</strong>o');
218 });
219
220 it('should by default bind view value to model even if not part of matches', function() {
221 var element = prepareInputEl('<div><input ng-model="result" uib-typeahead="item for item in source | filter:$viewValue"></div>');
222 changeInputValueTo(element, 'not in matches');
223 expect($scope.result).toEqual('not in matches');
224 });
225
226 it('should support the editable property to limit model bindings to matches only', function() {
227 var element = prepareInputEl('<div><input ng-model="result" uib-typeahead="item for item in source | filter:$viewValue" typeahead-editable="false"></div>');
228 changeInputValueTo(element, 'not in matches');
229 expect($scope.result).toEqual(undefined);
230 });
231
232 it('should set validation errors for non-editable inputs', function() {
233 var element = prepareInputEl(
234 '<div><form name="form">' +
235 '<input name="input" ng-model="result" uib-typeahead="item for item in source | filter:$viewValue" typeahead-editable="false">' +
236 '</form></div>');
237
238 changeInputValueTo(element, 'not in matches');
239 expect($scope.result).toEqual(undefined);
240 expect($scope.form.input.$error.editable).toBeTruthy();
241
242 changeInputValueTo(element, 'foo');
243 triggerKeyDown(element, 13);
244 expect($scope.result).toEqual('foo');
245 expect($scope.form.input.$error.editable).toBeFalsy();
246 });
247
248 it('should not set editable validation error for empty input', function() {
249 var element = prepareInputEl(
250 '<div><form name="form">' +
251 '<input name="input" ng-model="result" uib-typeahead="item for item in source | filter:$viewValue" typeahead-editable="false">' +
252 '</form></div>');
253
254 changeInputValueTo(element, 'not in matches');
255 expect($scope.result).toEqual(undefined);
256 expect($scope.form.input.$error.editable).toBeTruthy();
257 changeInputValueTo(element, '');
258 expect($scope.result).toEqual(null);
259 expect($scope.form.input.$error.editable).toBeFalsy();
260 });
261
262 it('should bind loading indicator expression', inject(function($timeout) {
263 $scope.isLoading = false;
264 $scope.loadMatches = function(viewValue) {
265 return $timeout(function() {
266 return [];
267 }, 1000);
268 };
269
270 var element = prepareInputEl('<div><input ng-model="result" uib-typeahead="item for item in loadMatches()" typeahead-loading="isLoading"></div>');
271 changeInputValueTo(element, 'foo');
272
273 expect($scope.isLoading).toBeTruthy();
274 $timeout.flush();
275 expect($scope.isLoading).toBeFalsy();
276 }));
277
278 it('should support timeout before trying to match $viewValue', inject(function($timeout) {
279 var element = prepareInputEl('<div><input ng-model="result" uib-typeahead="item for item in source | filter:$viewValue" typeahead-wait-ms="200"></div>');
280 changeInputValueTo(element, 'foo');
281 expect(element).toBeClosed();
282
283 $timeout.flush();
284 expect(element).toBeOpenWithActive(1, 0);
285 }));
286
287 it('should cancel old timeouts when something is typed within waitTime', inject(function($timeout) {
288 var values = [];
289 $scope.loadMatches = function(viewValue) {
290 values.push(viewValue);
291 return $scope.source;
292 };
293 var element = prepareInputEl('<div><input ng-model="result" uib-typeahead="item for item in loadMatches($viewValue) | filter:$viewValue" typeahead-wait-ms="200"></div>');
294 changeInputValueTo(element, 'first');
295 changeInputValueTo(element, 'second');
296
297 $timeout.flush();
298
299 expect(values).not.toContain('first');
300 }));
301
302 it('should allow timeouts when something is typed after waitTime has passed', inject(function($timeout) {
303 var values = [];
304
305 $scope.loadMatches = function(viewValue) {
306 values.push(viewValue);
307 return $scope.source;
308 };
309 var element = prepareInputEl('<div><input ng-model="result" uib-typeahead="item for item in loadMatches($viewValue) | filter:$viewValue" typeahead-wait-ms="200"></div>');
310
311 changeInputValueTo(element, 'first');
312 $timeout.flush();
313
314 expect(values).toContain('first');
315
316 changeInputValueTo(element, 'second');
317 $timeout.flush();
318
319 expect(values).toContain('second');
320 }));
321
322 it('should support custom popup templates', function() {
323 $templateCache.put('custom.html', '<div class="custom">foo</div>');
324
325 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>');
326
327 changeInputValueTo(element, 'Al');
328
329 expect(element.find('.custom').text()).toBe('foo');
330 });
331
332 it('should support custom templates for matched items', function() {
333 $templateCache.put('custom.html', '<p>{{ index }} {{ match.label }}</p>');
334
335 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>');
336
337 changeInputValueTo(element, 'Al');
338
339 expect(findMatches(element).eq(0).find('p').text()).toEqual('0 Alaska');
340 });
341
342 it('should support directives which require controllers in custom templates for matched items', function() {
343 $templateCache.put('custom.html', '<p child-directive>{{ index }} {{ match.label }}</p>');
344
345 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>');
346
347 element.data('$parentDirectiveController', {});
348
349 changeInputValueTo(element, 'Al');
350
351 expect(findMatches(element).eq(0).find('p').text()).toEqual('0 Alaska');
352 });
353
354 it('should throw error on invalid expression', function() {
355 var prepareInvalidDir = function() {
356 prepareInputEl('<div><input ng-model="result" uib-typeahead="an invalid expression"></div>');
357 };
358 expect(prepareInvalidDir).toThrow();
359 });
360 });
361
362 describe('selecting a match', function() {
363 it('should select a match on enter', function() {
364 var element = prepareInputEl('<div><input ng-model="result" uib-typeahead="item for item in source | filter:$viewValue"></div>');
365 var inputEl = findInput(element);
366
367 changeInputValueTo(element, 'b');
368 triggerKeyDown(element, 13);
369
370 expect($scope.result).toEqual('bar');
371 expect(inputEl.val()).toEqual('bar');
372 expect(element).toBeClosed();
373 });
374
375 it('should select a match on tab', function() {
376 var element = prepareInputEl('<div><input ng-model="result" uib-typeahead="item for item in source | filter:$viewValue"></div>');
377 var inputEl = findInput(element);
378
379 changeInputValueTo(element, 'b');
380 triggerKeyDown(element, 9);
381
382 expect($scope.result).toEqual('bar');
383 expect(inputEl.val()).toEqual('bar');
384 expect(element).toBeClosed();
385 });
386
387 it('should not select any match on blur without \'select-on-blur=true\' option', function() {
388 var element = prepareInputEl('<div><input ng-model="result" uib-typeahead="item for item in source | filter:$viewValue"></div>');
389 var inputEl = findInput(element);
390
391 changeInputValueTo(element, 'b');
392 inputEl.blur(); // input loses focus
393
394 // no change
395 expect($scope.result).toEqual('b');
396 expect(inputEl.val()).toEqual('b');
397 });
398
399 it('should select a match on blur with \'select-on-blur=true\' option', function() {
400 var element = prepareInputEl('<div><input ng-model="result" uib-typeahead="item for item in source | filter:$viewValue" typeahead-select-on-blur="true"></div>');
401 var inputEl = findInput(element);
402
403 changeInputValueTo(element, 'b');
404 inputEl.blur(); // input loses focus
405
406 // first element should be selected
407 expect($scope.result).toEqual('bar');
408 expect(inputEl.val()).toEqual('bar');
409 });
410
411 it('should select match on click', function() {
412 var element = prepareInputEl('<div><input ng-model="result" uib-typeahead="item for item in source | filter:$viewValue"></div>');
413 var inputEl = findInput(element);
414
415 changeInputValueTo(element, 'b');
416 var match = $(findMatches(element)[1]).find('a')[0];
417
418 $(match).click();
419 $scope.$digest();
420
421 expect($scope.result).toEqual('baz');
422 expect(inputEl.val()).toEqual('baz');
423 expect(element).toBeClosed();
424 });
425
426 it('should invoke select callback on select', function() {
427 $scope.onSelect = function($item, $model, $label) {
428 $scope.$item = $item;
429 $scope.$model = $model;
430 $scope.$label = $label;
431 };
432 var element = prepareInputEl('<div><input ng-model="result" typeahead-on-select="onSelect($item, $model, $label)" uib-typeahead="state.code as state.name for state in states | filter:$viewValue"></div>');
433
434 changeInputValueTo(element, 'Alas');
435 triggerKeyDown(element, 13);
436
437 expect($scope.result).toEqual('AL');
438 expect($scope.$item).toEqual($scope.states[0]);
439 expect($scope.$model).toEqual('AL');
440 expect($scope.$label).toEqual('Alaska');
441 });
442
443 it('should correctly update inputs value on mapping where label is not derived from the model', function() {
444 var element = prepareInputEl('<div><input ng-model="result" uib-typeahead="state.code as state.name for state in states | filter:$viewValue"></div>');
445 var inputEl = findInput(element);
446
447 changeInputValueTo(element, 'Alas');
448 triggerKeyDown(element, 13);
449
450 expect($scope.result).toEqual('AL');
451 expect(inputEl.val()).toEqual('AL');
452 });
453
454 it('should bind no results indicator as true when no matches returned', inject(function($timeout) {
455 $scope.isNoResults = false;
456 $scope.loadMatches = function(viewValue) {
457 return $timeout(function() {
458 return [];
459 }, 1000);
460 };
461
462 var element = prepareInputEl('<div><input ng-model="result" uib-typeahead="item for item in loadMatches()" typeahead-no-results="isNoResults"></div>');
463 changeInputValueTo(element, 'foo');
464
465 expect($scope.isNoResults).toBeFalsy();
466 $timeout.flush();
467 expect($scope.isNoResults).toBeTruthy();
468 }));
469
470 it('should bind no results indicator as false when matches are returned', inject(function($timeout) {
471 $scope.isNoResults = false;
472 $scope.loadMatches = function(viewValue) {
473 return $timeout(function() {
474 return [viewValue];
475 }, 1000);
476 };
477
478 var element = prepareInputEl('<div><input ng-model="result" uib-typeahead="item for item in loadMatches()" typeahead-no-results="isNoResults"></div>');
479 changeInputValueTo(element, 'foo');
480
481 expect($scope.isNoResults).toBeFalsy();
482 $timeout.flush();
483 expect($scope.isNoResults).toBeFalsy();
484 }));
485
486 it('should not focus the input if `typeahead-focus-on-select` is false', function() {
487 var element = prepareInputEl('<div><input ng-model="result" uib-typeahead="item for item in source | filter:$viewValue" typeahead-focus-on-select="false"></div>');
488 $document.find('body').append(element);
489 var inputEl = findInput(element);
490
491 changeInputValueTo(element, 'b');
492 var match = $(findMatches(element)[1]).find('a')[0];
493
494 $(match).click();
495 $scope.$digest();
496 $timeout.flush();
497
498 expect(document.activeElement).not.toBe(inputEl[0]);
499 expect($scope.result).toEqual('baz');
500 });
501 });
502
503 describe('select on exact match', function() {
504 it('should select on an exact match when set', function() {
505 $scope.onSelect = jasmine.createSpy('onSelect');
506 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>');
507 var inputEl = findInput(element);
508
509 changeInputValueTo(element, 'bar');
510
511 expect($scope.result).toEqual('bar');
512 expect(inputEl.val()).toEqual('bar');
513 expect(element).toBeClosed();
514 expect($scope.onSelect).toHaveBeenCalled();
515 });
516
517 it('should not select on an exact match by default', function() {
518 $scope.onSelect = jasmine.createSpy('onSelect');
519 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>');
520 var inputEl = findInput(element);
521
522 changeInputValueTo(element, 'bar');
523
524 expect($scope.result).toBeUndefined();
525 expect(inputEl.val()).toEqual('bar');
526 expect($scope.onSelect.calls.any()).toBe(false);
527 });
528
529 it('should not be case sensitive when select on an exact match', function() {
530 $scope.onSelect = jasmine.createSpy('onSelect');
531 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>');
532 var inputEl = findInput(element);
533
534 changeInputValueTo(element, 'BaR');
535
536 expect($scope.result).toEqual('bar');
537 expect(inputEl.val()).toEqual('bar');
538 expect(element).toBeClosed();
539 expect($scope.onSelect).toHaveBeenCalled();
540 });
541
542 it('should not auto select when not a match with one potential result left', function() {
543 $scope.onSelect = jasmine.createSpy('onSelect');
544 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>');
545 var inputEl = findInput(element);
546
547 changeInputValueTo(element, 'fo');
548
549 expect($scope.result).toBeUndefined();
550 expect(inputEl.val()).toEqual('fo');
551 expect($scope.onSelect.calls.any()).toBe(false);
552 });
553 });
554
555 describe('pop-up interaction', function() {
556 var element;
557
558 beforeEach(function() {
559 element = prepareInputEl('<div><input ng-model="result" uib-typeahead="item for item in source | filter:$viewValue"></div>');
560 });
561
562 it('should activate prev/next matches on up/down keys', function() {
563 changeInputValueTo(element, 'b');
564 expect(element).toBeOpenWithActive(2, 0);
565
566 // Down arrow key
567 triggerKeyDown(element, 40);
568 expect(element).toBeOpenWithActive(2, 1);
569
570 // Down arrow key goes back to first element
571 triggerKeyDown(element, 40);
572 expect(element).toBeOpenWithActive(2, 0);
573
574 // Up arrow key goes back to last element
575 triggerKeyDown(element, 38);
576 expect(element).toBeOpenWithActive(2, 1);
577
578 // Up arrow key goes back to first element
579 triggerKeyDown(element, 38);
580 expect(element).toBeOpenWithActive(2, 0);
581 });
582
583 it('should close popup on escape key', function() {
584 changeInputValueTo(element, 'b');
585 expect(element).toBeOpenWithActive(2, 0);
586
587 // Escape key
588 triggerKeyDown(element, 27);
589 expect(element).toBeClosed();
590 });
591
592 it('should highlight match on mouseenter', function() {
593 changeInputValueTo(element, 'b');
594 expect(element).toBeOpenWithActive(2, 0);
595
596 findMatches(element).eq(1).trigger('mouseenter');
597 expect(element).toBeOpenWithActive(2, 1);
598 });
599 });
600
601 describe('promises', function() {
602 var element, deferred;
603
604 beforeEach(inject(function($q) {
605 deferred = $q.defer();
606 $scope.source = function() {
607 return deferred.promise;
608 };
609 element = prepareInputEl('<div><input ng-model="result" uib-typeahead="item for item in source()"></div>');
610 }));
611
612 it('should display matches from promise', function() {
613 changeInputValueTo(element, 'c');
614 expect(element).toBeClosed();
615
616 deferred.resolve(['good', 'stuff']);
617 $scope.$digest();
618 expect(element).toBeOpenWithActive(2, 0);
619 });
620
621 it('should not display anything when promise is rejected', function() {
622 changeInputValueTo(element, 'c');
623 expect(element).toBeClosed();
624
625 deferred.reject('fail');
626 $scope.$digest();
627 expect(element).toBeClosed();
628 });
629
630 it('PR #3178, resolves #2999 - should not return property "length" of undefined for undefined matches', function() {
631 changeInputValueTo(element, 'c');
632 expect(element).toBeClosed();
633
634 deferred.resolve();
635 $scope.$digest();
636 expect(element).toBeClosed();
637 });
638 });
639
640 describe('non-regressions tests', function() {
641
642 it('issue 231 - closes matches popup on click outside typeahead', function() {
643 var element = prepareInputEl('<div><input ng-model="result" uib-typeahead="item for item in source | filter:$viewValue"></div>');
644
645 changeInputValueTo(element, 'b');
646
647 $document.find('body').click();
648 $scope.$digest();
649
650 expect(element).toBeClosed();
651 });
652
653 it('issue 591 - initial formatting for un-selected match and complex label expression', function() {
654 var inputEl = findInput(prepareInputEl('<div><input ng-model="result" uib-typeahead="state as state.name + \' \' + state.code for state in states | filter:$viewValue"></div>'));
655 expect(inputEl.val()).toEqual('');
656 });
657
658 it('issue 786 - name of internal model should not conflict with scope model name', function() {
659 $scope.state = $scope.states[0];
660 var element = prepareInputEl('<div><input ng-model="state" uib-typeahead="state as state.name for state in states | filter:$viewValue"></div>');
661 var inputEl = findInput(element);
662
663 expect(inputEl.val()).toEqual('Alaska');
664 });
665
666 it('issue 863 - it should work correctly with input type="email"', function() {
667 $scope.emails = ['foo@host.com', 'bar@host.com'];
668 var element = prepareInputEl('<div><input type="email" ng-model="email" uib-typeahead="email for email in emails | filter:$viewValue"></div>');
669 var inputEl = findInput(element);
670
671 changeInputValueTo(element, 'bar');
672 expect(element).toBeOpenWithActive(1, 0);
673
674 triggerKeyDown(element, 13);
675
676 expect($scope.email).toEqual('bar@host.com');
677 expect(inputEl.val()).toEqual('bar@host.com');
678 });
679
680 it('issue 964 - should not show popup with matches if an element is not focused', function() {
681 $scope.items = function(viewValue) {
682 return $timeout(function() {
683 return [viewValue];
684 });
685 };
686 var element = prepareInputEl('<div><input ng-model="result" uib-typeahead="item for item in items($viewValue)"></div>');
687 var inputEl = findInput(element);
688
689 changeInputValueTo(element, 'match');
690 $scope.$digest();
691
692 inputEl.blur();
693 $timeout.flush();
694
695 expect(element).toBeClosed();
696 });
697
698 it('should properly update loading callback if an element is not focused', function() {
699 $scope.items = function(viewValue) {
700 return $timeout(function(){
701 return [viewValue];
702 });
703 };
704 var element = prepareInputEl('<div><input ng-model="result" typeahead-loading="isLoading" uib-typeahead="item for item in items($viewValue)"></div>');
705 var inputEl = findInput(element);
706
707 changeInputValueTo(element, 'match');
708 $scope.$digest();
709
710 inputEl.blur();
711 $timeout.flush();
712
713 expect($scope.isLoading).toBeFalsy();
714 });
715
716 it('issue 1140 - should properly update loading callback when deleting characters', function() {
717 $scope.items = function(viewValue) {
718 return $timeout(function() {
719 return [viewValue];
720 });
721 };
722 var element = prepareInputEl('<div><input ng-model="result" typeahead-min-length="2" typeahead-loading="isLoading" uib-typeahead="item for item in items($viewValue)"></div>');
723
724 changeInputValueTo(element, 'match');
725 $scope.$digest();
726
727 expect($scope.isLoading).toBeTruthy();
728
729 changeInputValueTo(element, 'm');
730 $timeout.flush();
731 $scope.$digest();
732
733 expect($scope.isLoading).toBeFalsy();
734 });
735
736 it('should cancel old timeout when deleting characters', inject(function($timeout) {
737 var values = [];
738 $scope.loadMatches = function(viewValue) {
739 values.push(viewValue);
740 return $scope.source;
741 };
742 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>');
743 changeInputValueTo(element, 'match');
744 changeInputValueTo(element, 'm');
745
746 $timeout.flush();
747
748 expect(values).not.toContain('match');
749 }));
750
751 describe('', function() {
752 // Dummy describe to be able to create an after hook for this tests
753 var element;
754
755 it('does not close matches popup on click in input', function() {
756 element = prepareInputEl('<div><input ng-model="result" uib-typeahead="item for item in source | filter:$viewValue"></div>');
757 var inputEl = findInput(element);
758
759 // Note that this bug can only be found when element is in the document
760 $document.find('body').append(element);
761
762 changeInputValueTo(element, 'b');
763
764 inputEl.click();
765 $scope.$digest();
766
767 expect(element).toBeOpenWithActive(2, 0);
768 });
769
770 it('issue #1773 - should not trigger an error when used with ng-focus', function() {
771 element = prepareInputEl('<div><input ng-model="result" uib-typeahead="item for item in source | filter:$viewValue" ng-focus="foo()"></div>');
772 var inputEl = findInput(element);
773
774 // Note that this bug can only be found when element is in the document
775 $document.find('body').append(element);
776
777 changeInputValueTo(element, 'b');
778 var match = $(findMatches(element)[1]).find('a')[0];
779
780 $(match).click();
781 $scope.$digest();
782 });
783
784 afterEach(function() {
785 element.remove();
786 });
787 });
788
789 it('issue #1238 - allow names like "query" to be used inside "in" expressions ', function() {
790 $scope.query = function() {
791 return ['foo', 'bar'];
792 };
793
794 var element = prepareInputEl('<div><input ng-model="result" uib-typeahead="item for item in query($viewValue)"></div>');
795 changeInputValueTo(element, 'bar');
796
797 expect(element).toBeOpenWithActive(2, 0);
798 });
799
800 it('issue #3318 - should set model validity to true when set manually', function() {
801 var element = prepareInputEl(
802 '<div><form name="form">' +
803 '<input name="input" ng-model="result" uib-typeahead="item for item in source | filter:$viewValue" typeahead-editable="false">' +
804 '</form></div>');
805
806 changeInputValueTo(element, 'not in matches');
807 $scope.$apply(function() {
808 $scope.result = 'manually set';
809 });
810
811 expect($scope.result).toEqual('manually set');
812 expect($scope.form.input.$valid).toBeTruthy();
813 });
814
815 it('issue #3166 - should set \'parse\' key as valid when selecting a perfect match and not editable', function() {
816 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>');
817 var inputEl = findInput(element);
818
819 changeInputValueTo(element, 'Alaska');
820 triggerKeyDown(element, 13);
821
822 expect($scope.test.typeahead.$error.parse).toBeUndefined();
823 });
824
825 it('issue #3823 - should support ng-model-options getterSetter', function() {
826 function resultSetter(state) {
827 return state;
828 }
829 $scope.result = resultSetter;
830 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>');
831
832 changeInputValueTo(element, 'Alaska');
833 triggerKeyDown(element, 13);
834
835 expect($scope.result).toBe(resultSetter);
836 });
837 });
838
839 describe('input formatting', function() {
840 it('should co-operate with existing formatters', function() {
841 $scope.result = $scope.states[0];
842
843 var element = prepareInputEl('<div><input ng-model="result.name" formatter uib-typeahead="state.name for state in states | filter:$viewValue"></div>'),
844 inputEl = findInput(element);
845
846 expect(inputEl.val()).toEqual('formatted' + $scope.result.name);
847 });
848
849 it('should support a custom input formatting function', function() {
850 $scope.result = $scope.states[0];
851 $scope.formatInput = function($model) {
852 return $model.code;
853 };
854
855 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>'),
856 inputEl = findInput(element);
857
858 expect(inputEl.val()).toEqual('AL');
859 expect($scope.result).toEqual($scope.states[0]);
860 });
861 });
862
863 describe('append to element id', function() {
864 it('append typeahead results to element', function() {
865 $document.find('body').append('<div id="myElement"></div>');
866 var element = prepareInputEl('<div><input name="input" ng-model="result" uib-typeahead="item for item in states | filter:$viewValue" typeahead-append-to-element-id="myElement"></div>');
867 changeInputValueTo(element, 'al');
868 expect($document.find('#myElement')).toBeOpenWithActive(2, 0);
869 $document.find('#myElement').remove();
870 });
871 });
872
873 describe('append to body', function() {
874 afterEach(function() {
875 angular.element($window).off('resize');
876 $document.find('body').off('scroll');
877 });
878
879 it('append typeahead results to body', function() {
880 var element = prepareInputEl('<div><input ng-model="result" uib-typeahead="item for item in source | filter:$viewValue" typeahead-append-to-body="true"></div>');
881 changeInputValueTo(element, 'ba');
882 expect($document.find('body')).toBeOpenWithActive(2, 0);
883 });
884
885 it('should not append to body when value of the attribute is false', function() {
886 var element = prepareInputEl('<div><input ng-model="result" uib-typeahead="item for item in source | filter:$viewValue" typeahead-append-to-body="false"></div>');
887 changeInputValueTo(element, 'ba');
888 expect(findDropDown($document.find('body')).length).toEqual(0);
889 });
890
891 it('should have right position after scroll', function() {
892 var element = prepareInputEl('<div><input ng-model="result" uib-typeahead="item for item in source | filter:$viewValue" typeahead-append-to-body="true"></div>');
893 var dropdown = findDropDown($document.find('body'));
894 var body = angular.element(document.body);
895
896 // Set body height to allow scrolling
897 body.css({height:'10000px'});
898
899 // Scroll top
900 window.scroll(0, 1000);
901
902 // Set input value to show dropdown
903 changeInputValueTo(element, 'ba');
904
905 // Init position of dropdown must be 1000px
906 expect(dropdown.css('top') ).toEqual('1000px');
907
908 // After scroll, must have new position
909 window.scroll(0, 500);
910 body.triggerHandler('scroll');
911 $timeout.flush();
912 expect(dropdown.css('top')).toEqual('500px');
913 });
914 });
915
916 describe('focus first', function() {
917 it('should focus the first element by default', function() {
918 var element = prepareInputEl('<div><input ng-model="result" uib-typeahead="item for item in source | filter:$viewValue"></div>');
919 changeInputValueTo(element, 'b');
920 expect(element).toBeOpenWithActive(2, 0);
921
922 // Down arrow key
923 triggerKeyDown(element, 40);
924 expect(element).toBeOpenWithActive(2, 1);
925
926 // Down arrow key goes back to first element
927 triggerKeyDown(element, 40);
928 expect(element).toBeOpenWithActive(2, 0);
929
930 // Up arrow key goes back to last element
931 triggerKeyDown(element, 38);
932 expect(element).toBeOpenWithActive(2, 1);
933
934 // Up arrow key goes back to first element
935 triggerKeyDown(element, 38);
936 expect(element).toBeOpenWithActive(2, 0);
937 });
938
939 it('should not focus the first element until keys are pressed', function() {
940 var element = prepareInputEl('<div><input ng-model="result" uib-typeahead="item for item in source | filter:$viewValue" typeahead-focus-first="false"></div>');
941 changeInputValueTo(element, 'b');
942 expect(element).toBeOpenWithActive(2, -1);
943
944 // Down arrow key goes to first element
945 triggerKeyDown(element, 40);
946 expect(element).toBeOpenWithActive(2, 0);
947
948 // Down arrow key goes to second element
949 triggerKeyDown(element, 40);
950 expect(element).toBeOpenWithActive(2, 1);
951
952 // Down arrow key goes back to first element
953 triggerKeyDown(element, 40);
954 expect(element).toBeOpenWithActive(2, 0);
955
956 // Up arrow key goes back to last element
957 triggerKeyDown(element, 38);
958 expect(element).toBeOpenWithActive(2, 1);
959
960 // Up arrow key goes back to first element
961 triggerKeyDown(element, 38);
962 expect(element).toBeOpenWithActive(2, 0);
963
964 // New input goes back to no focus
965 changeInputValueTo(element, 'a');
966 changeInputValueTo(element, 'b');
967 expect(element).toBeOpenWithActive(2, -1);
968
969 // Up arrow key goes to last element
970 triggerKeyDown(element, 38);
971 expect(element).toBeOpenWithActive(2, 1);
972 });
973 });
974
975 it('should not capture enter or tab when an item is not focused', function() {
976 $scope.select_count = 0;
977 $scope.onSelect = function($item, $model, $label) {
978 $scope.select_count = $scope.select_count + 1;
979 };
980 var element = prepareInputEl('<div><input ng-model="result" ng-keydown="keyDownEvent = $event" uib-typeahead="item for item in source | filter:$viewValue" typeahead-on-select="onSelect($item, $model, $label)" typeahead-focus-first="false"></div>');
981 changeInputValueTo(element, 'b');
982
983 // enter key should not be captured when nothing is focused
984 triggerKeyDown(element, 13);
985 expect($scope.keyDownEvent.isDefaultPrevented()).toBeFalsy();
986 expect($scope.select_count).toEqual(0);
987
988 // tab key should close the dropdown when nothing is focused
989 triggerKeyDown(element, 9);
990 expect($scope.keyDownEvent.isDefaultPrevented()).toBeFalsy();
991 expect($scope.select_count).toEqual(0);
992 expect(element).toBeClosed();
993 });
994
995 it('should capture enter or tab when an item is focused', function() {
996 $scope.select_count = 0;
997 $scope.onSelect = function($item, $model, $label) {
998 $scope.select_count = $scope.select_count + 1;
999 };
1000 var element = prepareInputEl('<div><input ng-model="result" ng-keydown="keyDownEvent = $event" uib-typeahead="item for item in source | filter:$viewValue" typeahead-on-select="onSelect($item, $model, $label)" typeahead-focus-first="false"></div>');
1001 changeInputValueTo(element, 'b');
1002
1003 // down key should be captured and focus first element
1004 triggerKeyDown(element, 40);
1005 expect($scope.keyDownEvent.isDefaultPrevented()).toBeTruthy();
1006 expect(element).toBeOpenWithActive(2, 0);
1007
1008 // enter key should be captured now that something is focused
1009 triggerKeyDown(element, 13);
1010 expect($scope.keyDownEvent.isDefaultPrevented()).toBeTruthy();
1011 expect($scope.select_count).toEqual(1);
1012 });
1013
1014 describe('minLength set to 0', function() {
1015 it('should open typeahead if input is changed to empty string if defined threshold is 0', function() {
1016 var element = prepareInputEl('<div><input ng-model="result" uib-typeahead="item for item in source | filter:$viewValue" typeahead-min-length="0"></div>');
1017 changeInputValueTo(element, '');
1018 expect(element).toBeOpenWithActive(3, 0);
1019 });
1020 });
1021
1022 describe('event listeners', function() {
1023 afterEach(function() {
1024 angular.element($window).off('resize');
1025 $document.find('body').off('scroll');
1026 });
1027
1028 it('should register event listeners when attached to body', function() {
1029 spyOn(window, 'addEventListener');
1030 spyOn(document.body, 'addEventListener');
1031
1032 var element = prepareInputEl('<div><input ng-model="result" uib-typeahead="item for item in source | filter:$viewValue" typeahead-append-to-body="true"></div>');
1033
1034 expect(window.addEventListener).toHaveBeenCalledWith('resize', jasmine.any(Function), false);
1035 expect(document.body.addEventListener).toHaveBeenCalledWith('scroll', jasmine.any(Function), false);
1036 });
1037
1038 it('should remove event listeners when attached to body', function() {
1039 spyOn(window, 'removeEventListener');
1040 spyOn(document.body, 'removeEventListener');
1041
1042 var element = prepareInputEl('<div><input ng-model="result" uib-typeahead="item for item in source | filter:$viewValue" typeahead-append-to-body="true"></div>');
1043 $scope.$destroy();
1044
1045 expect(window.removeEventListener).toHaveBeenCalledWith('resize', jasmine.any(Function), false);
1046 expect(document.body.removeEventListener).toHaveBeenCalledWith('scroll', jasmine.any(Function), false);
1047 });
1048 });
1049});
1050
1051/* Deprecation tests below */
1052
1053describe('typeahead deprecation', function() {
1054 beforeEach(module('ui.bootstrap.typeahead'));
1055 beforeEach(module('ngSanitize'));
1056 beforeEach(module('template/typeahead/typeahead-popup.html'));
1057 beforeEach(module('template/typeahead/typeahead-match.html'));
1058
1059 it('should suppress warning', function() {
1060 module(function($provide) {
1061 $provide.value('$typeaheadSuppressWarning', true);
1062 });
1063
1064 inject(function($compile, $log, $rootScope) {
1065 spyOn($log, 'warn');
1066
1067 var element = '<div><input ng-model="result" typeahead="item for item in source | filter:$viewValue" typeahead-min-length="0"></div>';
1068 element = $compile(element)($rootScope);
1069 $rootScope.$digest();
1070 expect($log.warn.calls.count()).toBe(0);
1071 });
1072 });
1073
1074 it('should give warning by default', inject(function($compile, $log, $rootScope) {
1075 spyOn($log, 'warn');
1076
1077 var element = '<div><input ng-model="result" typeahead="item for item in source | filter:$viewValue" typeahead-min-length="0"></div>';
1078 element = $compile(element)($rootScope);
1079 $rootScope.$digest();
1080
1081 expect($log.warn.calls.count()).toBe(3);
1082 expect($log.warn.calls.argsFor(0)).toEqual(['typeaheadParser is now deprecated. Use uibTypeaheadParser instead.']);
1083 expect($log.warn.calls.argsFor(1)).toEqual(['typeahead is now deprecated. Use uib-typeahead instead.']);
1084 expect($log.warn.calls.argsFor(2)).toEqual(['typeahead-popup is now deprecated. Use uib-typeahead-popup instead.']);
1085 }));
1086
1087 it('should deprecate typeaheadMatch', inject(function($compile, $log, $rootScope, $templateCache, $sniffer){
1088 spyOn($log, 'warn');
1089
1090 var element = '<div typeahead-match index=\"$index\" match=\"match\" query=\"query\" template-url=\"templateUrl\"></div>';
1091 element = $compile(element)($rootScope);
1092 $rootScope.$digest();
1093
1094 expect($log.warn.calls.count()).toBe(1);
1095 expect($log.warn.calls.argsFor(0)).toEqual(['typeahead-match is now deprecated. Use uib-typeahead-match instead.']);
1096 }));
1097});