UNPKG

23.9 kBJavaScriptView Raw
1/* global inject */
2/* eslint-disable no-magic-numbers */
3
4import angular from 'angular';
5import 'angular-mocks';
6
7import Select from '../select/select';
8import styles from '../select/select.css';
9
10import SelectNg from './select-ng';
11
12describe('Select Ng', () => {
13 let scope;
14 let element;
15 let ctrl;
16 let $compile;
17
18 const fakeItems = [
19 {id: 1, name: '11'},
20 {id: 2, name: '22'},
21 {id: 3, name: '33'}
22 ];
23
24 beforeEach(window.module(SelectNg));
25
26 function compileTemplate(template) {
27 element = $compile(template)(scope);
28 ctrl = element.controller('rgSelect');
29 scope.$digest();
30 }
31
32 beforeEach(inject(($rootScope, _$compile_) => {
33 scope = $rootScope.$new();
34 $compile = _$compile_;
35
36 scope.items = angular.copy(fakeItems);
37 scope.selectedItem = scope.items[2];
38 scope.selectedItems = scope.items.slice(1);
39
40 compileTemplate('<rg-select options="item.name for item in items track by item.id" ng-model="selectedItem"></rg-select>');
41 }));
42
43 describe('DOM', () => {
44 it('Should place container for select inside directive', () => {
45 element[0].should.contain('span');
46 });
47
48 it('Should render select inside container', () => {
49 element[0].should.contain('[data-test=ring-select]');
50 });
51
52 it('Should not render select if type=dropdown', () => {
53 compileTemplate('<rg-select options="item.name for item in items track by item.id" ng-model="selectedItem" type="dropdown"></rg-select>');
54
55 element[0].should.not.contain('[data-test=ring-select]');
56 });
57 });
58
59 describe('Interface', () => {
60 function selected(_ctrl) {
61 return _ctrl.selectInstance.props.selected;
62 }
63
64 it('Should unmount react component on destroy', () => {
65 initializeReactSelect(element[0]);
66 ctrl.$onDestroy();
67 should.not.exist(ctrl.selectInstance.node);
68 });
69
70 it('Should receive ngModel controller', () => {
71 ctrl.ngModelCtrl.should.exist;
72 });
73
74 it('Should extend passed config', () => {
75 scope.config = {someField: 'test'};
76 compileTemplate('<rg-select options="item.name for item in items track by item.id" ng-model="selectedItem" config="config"></rg-select>');
77
78 ctrl.config.someField.should.equal('test');
79 });
80
81 it('Should work without ng-model', () => {
82 function initDirective() {
83 compileTemplate('<rg-select options="item.name for item in items track by item.id"></rg-select>');
84 }
85
86 initDirective.should.not.throw;
87 });
88
89 it('Should update selected item on ngModel updates', () => {
90 const newLabel = 'Some new label';
91 scope.selectedItem.name = newLabel;
92 scope.$digest();
93 selected(ctrl).label.should.equal(newLabel);
94 });
95
96 it('Should clear selected item on ngModel clearing', () => {
97 scope.selectedItem = null;
98 scope.$digest();
99 should.not.exist(selected(ctrl));
100 });
101
102 it('Should not get options on on initialization', () => {
103 ctrl.selectInstance.props.data.length.should.equal(0);
104 });
105
106 it('Should get options on open', () => {
107 ctrl.config.onBeforeOpen();
108 scope.$digest();
109 ctrl.selectInstance.props.data.length.should.equal(fakeItems.length);
110 });
111
112 it('Should convert ngModel to select supported object', () => {
113 selected(ctrl).key.should.equal(scope.selectedItem.id);
114 selected(ctrl).label.should.equal(scope.selectedItem.name);
115 });
116
117 it('Should convert options to select supported objects', () => {
118 ctrl.loadOptionsToSelect();
119 scope.$digest();
120 ctrl.selectInstance.props.data[0].key.should.equal(fakeItems[0].id);
121 ctrl.selectInstance.props.data[0].label.should.equal(fakeItems[0].name);
122 });
123
124 it('Should reject promise on loading error', inject($q => {
125 const onError = sandbox.spy();
126 sandbox.stub(ctrl, 'getOptions').returns($q.reject());
127 ctrl.loadOptionsToSelect().catch(onError);
128 scope.$digest();
129 onError.should.be.called;
130 }));
131
132 it('Should use default type "Button" if type is not passed', () => {
133 compileTemplate('<rg-select options="item.name for item in items track by item.id" ng-model="selectedItem"></rg-select>');
134 ctrl.selectInstance.props.type.should.equal(Select.Type.MATERIAL);
135 });
136
137 it('Should support type "input"', () => {
138 compileTemplate('<rg-select options="item.name for item in items track by item.id" ng-model="selectedItem" type="input"></rg-select>');
139 ctrl.selectInstance.props.type.should.equal(Select.Type.INPUT);
140 });
141
142 it('Should support type "dropdown', () => {
143 compileTemplate('<rg-select options="item.name for item in items track by item.id" ng-model="selectedItem" type="dropdown"></rg-select>');
144 ctrl.selectInstance.props.type.should.equal(Select.Type.CUSTOM);
145 });
146
147 it('Should support selectedLabelField customization', () => {
148 scope.selectedItem.testField = 'test';
149 compileTemplate('<rg-select options="item.name select as item.testField for item in items track by item.id" ng-model="selectedItem"></rg-select>');
150 selected(ctrl).selectedLabel.should.equal('test');
151 });
152
153 it('Should support selected formatter function', () => {
154 scope.formatter = sandbox.stub().returns('Formatted label');
155
156 compileTemplate('<rg-select options="item.name select as formatter(item) for item in items track by item.id" external-filter="true" ng-model="selectedItem"></rg-select>');
157
158 selected(ctrl).selectedLabel.should.equal('Formatted label');
159 });
160
161 it('Should support description customization', () => {
162 scope.selectedItem.testField = 'test';
163 compileTemplate('<rg-select options="item.name describe as item.testField for item in items track by item.id" ng-model="selectedItem"></rg-select>');
164
165 selected(ctrl).description.should.equal('test');
166 });
167
168 it('Should support description and selected label customization together', () => {
169 scope.selectedItem.selectText = 'test';
170 scope.selectedItem.descriptionText = 'description';
171 compileTemplate('<rg-select options="item.name select as item.selectText describe as item.descriptionText for item in items track by item.id" ng-model="selectedItem"></rg-select>');
172
173 selected(ctrl).selectedLabel.should.equal(scope.selectedItem.selectText);
174 selected(ctrl).description.should.equal(scope.selectedItem.descriptionText);
175 });
176
177 it('Should not call get option by value for description customization', () => {
178 scope.selectedItem.descriptionText = 'description';
179
180 element = $compile('<rg-select options="item.name describe as item.descriptionText for item in items track by item.id" ng-model="selectedItem"></rg-select>')(scope);
181 ctrl = element.controller('rgSelect');
182 sandbox.spy(ctrl.optionsParser, 'getOptions');
183 scope.$digest();
184
185 ctrl.optionsParser.getOptions.should.not.called;
186 });
187
188 it('Should not call get option by value for select label customization', () => {
189 scope.selectedItem.selectText = 'Test';
190
191 element = $compile('<rg-select options="item.name select as item.descriptionText for item in items track by item.id" ng-model="selectedItem"></rg-select>')(scope);
192 ctrl = element.controller('rgSelect');
193 sandbox.spy(ctrl.optionsParser, 'getOptions');
194 scope.$digest();
195
196 ctrl.optionsParser.getOptions.should.not.called;
197 });
198
199 it('Should not call get option by value for description and selected label customization together', () => {
200 scope.selectedItem.selectText = 'test';
201 scope.selectedItem.descriptionText = 'description';
202
203 element = $compile('<rg-select options="item.name select as item.selectText describe as item.descriptionText for item in items track by item.id" ng-model="selectedItem"></rg-select>')(scope);
204 ctrl = element.controller('rgSelect');
205 sandbox.spy(ctrl.optionsParser, 'getOptions');
206 scope.$digest();
207
208 ctrl.optionsParser.getOptions.should.not.called;
209 });
210
211 it('Should save original model in select items', () => {
212 ctrl.loadOptionsToSelect();
213 scope.$digest();
214 ctrl.selectInstance.props.data[0].originalModel.should.deep.equal(fakeItems[0]);
215 });
216
217 it('Should update ng-model on selecting', () => {
218 ctrl.config.onChange({originalModel: fakeItems[0]});
219 scope.$digest();
220 scope.selectedItem.should.equal(fakeItems[0]);
221 });
222
223 it('Should clear ng-model on clearing select', () => {
224 ctrl.config.onChange(null);
225 scope.$digest();
226 should.not.exist(scope.selectedItem);
227 });
228
229 it('Should call datasource on opening', () => {
230 scope.dataSource = sandbox.stub().returns(fakeItems);
231
232 compileTemplate('<rg-select options="item.name for item in dataSource(query) track by item.id" external-filter="true" ng-model="selectedItem"></rg-select>');
233
234 ctrl.config.onBeforeOpen();
235 scope.$digest();
236 scope.dataSource.should.have.been.called;
237 });
238
239 it('Should call datasource on filtering if external filter enabled', () => {
240 scope.dataSource = sandbox.stub().returns(fakeItems);
241
242 compileTemplate('<rg-select options="item.name for item in dataSource(query) track by item.id" external-filter="true" ng-model="selectedItem"></rg-select>');
243
244 ctrl.config.onFilter('test');
245 scope.$digest();
246 scope.dataSource.should.have.been.calledWith('test');
247 });
248
249 it('Should reload options with a controller query', () => {
250 const queryMock = 'query';
251 ctrl.query = queryMock;
252 sandbox.spy(ctrl, 'loadOptionsToSelect');
253
254 ctrl.config.reloadOptions();
255 scope.$digest();
256
257 ctrl.loadOptionsToSelect.should.have.been.calledWith(queryMock);
258 });
259
260 it('Should reload options with a provided query parameter', () => {
261 const queryMock = 'query';
262 ctrl.query = 'ctrlQuery';
263 sandbox.spy(ctrl, 'loadOptionsToSelect');
264
265 ctrl.config.reloadOptions(queryMock);
266 scope.$digest();
267
268 ctrl.loadOptionsToSelect.should.have.been.calledWith(queryMock);
269 });
270
271 it('If externalFilter enabled should provide custom filter.fn which should always return true', () => {
272 compileTemplate('<rg-select options="item.name for item in items track by item.id" external-filter="true" ng-model="selectedItem"></rg-select>');
273
274 ctrl.filter.fn().should.be.true;
275 });
276
277 it('Should be disabled if disabled', () => {
278 compileTemplate('<rg-select options="item.name for item in items track by item.id" ng-model="selectedItem" ng-disabled="true"></rg-select>');
279
280 element[0].should.contain(`.${styles.disabled}`);
281 });
282
283 it('Should hide on route changes ($locationChangeSuccess)', () => {
284 sandbox.stub(ctrl.selectInstance._popup, 'isVisible').returns(true);
285 ctrl.selectInstance._hidePopup = sandbox.stub();
286
287 scope.$broadcast('$locationChangeSuccess');
288 ctrl.selectInstance._hidePopup.should.have.been.called;
289 });
290
291 it('Should not try to hide on route changes if not showed ($locationChangeSuccess)', () => {
292 sandbox.stub(ctrl.selectInstance._popup, 'isVisible').returns(false);
293 ctrl.selectInstance._hidePopup = sandbox.stub();
294
295 scope.$broadcast('$locationChangeSuccess');
296 ctrl.selectInstance._hidePopup.should.not.have.been.called;
297 });
298
299 it('Should extend select model with properties from ng model', () => {
300 const selectModel = ctrl.convertNgModelToSelect({ext: 'test'});
301 selectModel.ext.should.equal('test');
302 });
303
304 it('Should not try to extend select model with string', () => {
305 sandbox.spy(angular, 'extend');
306 const stringValue = 'str-value';
307 ctrl.convertNgModelToSelect(stringValue);
308 angular.extend.should.been.calledWith(sandbox.match({}), null);
309 });
310
311 it('Should use select-type if defined', () => {
312 compileTemplate('<button rg-select="" options="itemvar in items track by itemvar.id" select-type="dropdown" type="submit"></button>');
313
314 ctrl.selectInstance.props.type.should.equal(Select.Type.CUSTOM);
315 });
316
317 it('Should use "multiple" attribute and provide it to select', () => {
318 compileTemplate('<rg-select options="item.name for item in items track by item.id" multiple="true" ng-model="selectedItems"></rg-select>');
319
320 ctrl.selectInstance.props.multiple.should.be.true;
321 });
322
323 it('Should watch "multiple" and update select after change', () => {
324 scope.selectMultiple = false;
325 compileTemplate('<rg-select options="item.name for item in items track by item.id" multiple="selectMultiple" ng-model="selectedItems"></rg-select>');
326
327 scope.selectMultiple = true;
328 scope.$digest();
329 ctrl.selectInstance.props.multiple.should.be.true;
330 });
331
332 it('Should return deselected item', () => {
333 scope.selectedItem = scope.items[1];
334 ctrl.config.onDeselect({originalModel: fakeItems[1]});
335 scope.$digest();
336
337 ctrl.selectInstance.props.selected.label.should.equal(fakeItems[1].name);
338 });
339
340 it('Should rerender with new config if config changed and autosync enabled', () => {
341 scope.config = {};
342 compileTemplate('<rg-select options="item.name for item in items track by item.id" ng-model="selectedItem" config="config" config-auto-update="true"></rg-select>');
343
344 sandbox.spy(ctrl.selectInstance, 'rerender');
345 scope.config.add = {label: 'fooo'};
346 scope.$digest();
347
348 ctrl.selectInstance.rerender.should.have.been.calledWith(sinon.match({add: {label: 'fooo'}}));
349 });
350
351
352 it('Should correctly reinitialize select with config', () => {
353 const template = '<rg-select type="dropdown" options="item.name for item in items track by item.id" ng-model="selectedItem" config="config" config-auto-update="true"></rg-select>';
354 scope.config = {};
355
356 compileTemplate(template);
357 initializeReactSelect(element[0]);
358 scope.$digest();
359 ctrl.$onDestroy();
360
361 compileTemplate(template);
362 initializeReactSelect(element[0]);
363 scope.$digest();
364 });
365
366
367 it('Should update config and do not loose new selected items', () => {
368 scope.config = {someField: 'AAA'};
369 scope.selectedItem = [scope.items[1]];
370 compileTemplate('<rg-select multiple=true options="item.name for item in items track by item.id" ng-model="selectedItem" config="config" config-auto-update="true"></rg-select>');
371
372 selected(ctrl).length.should.equal(1);
373
374 const newItems = [scope.items[0], scope.items[1]];
375 scope.selectedItem = newItems;
376 scope.$digest();
377 selected(ctrl).length.should.equal(newItems.length);
378
379 scope.config.someField = 'BBB';
380 scope.$digest();
381
382 selected(ctrl).length.should.equal(newItems.length);
383 });
384 });
385
386 describe('Options parser', () => {
387 it('Should support syntax "item in items"', () => {
388 scope.items = [{key: 1, label: 'test1'}];
389 scope.selectedItem = scope.items[0];
390
391 compileTemplate('<rg-select options="item in items" ng-model="selectedItem"></rg-select>');
392 ctrl.config.onBeforeOpen();
393 scope.$digest();
394 ctrl.selectInstance.props.data.length.should.equal(1);
395 });
396
397 it('Should support "item for item in items"', () => {
398 scope.items = [{key: 1, label: 'test1'}];
399 scope.selectedItem = scope.items[0];
400 compileTemplate('<rg-select options="item as item.label for item in items" ng-model="selectedItem"></rg-select>');
401 ctrl.config.onBeforeOpen();
402 scope.$digest();
403 ctrl.selectInstance.props.data[0].key.should.equal(scope.items[0].key);
404 });
405
406 it('Should support labeling item', () => {
407 compileTemplate('<rg-select options="item.name for item in items track by item.id" ng-model="selectedItem"></rg-select>');
408 ctrl.selectInstance.props.selected.label.should.equal(fakeItems[2].name);
409 });
410
411 it('Should support labeling item simple syntax', () => {
412 scope.items = [{key: 1, name: 'test1'}];
413 scope.selectedItem = scope.items[0];
414 compileTemplate('<rg-select options="item.name for item in items" ng-model="selectedItem"></rg-select>');
415 ctrl.selectInstance.props.selected.label.should.equal('test1');
416 });
417
418 it('Should support function as label', () => {
419 scope.getLabel = sandbox.stub().returns('test label');
420
421 compileTemplate('<rg-select options="getLabel(item) for item in items track by item.id" ng-model="selectedItem"></rg-select>');
422
423 ctrl.optionsParser.getLabel(fakeItems[0]).should.equal('test label');
424 scope.getLabel.should.been.calledWith(fakeItems[0]);
425 });
426
427 it('Should support custom key field with "track by" expression', () => {
428 scope.items = [{id: 1, label: 'test1'}];
429 scope.selectedItem = scope.items[0];
430 compileTemplate('<rg-select options="item in items track by item.id" ng-model="selectedItem"></rg-select>');
431
432 ctrl.optionsParser.getKey(scope.selectedItem).should.equal(scope.selectedItem.id);
433 });
434
435 it('Should support custom label field', () => {
436 scope.options = [{key: 1, name: 'testname'}];
437
438 compileTemplate('<rg-select options="item.name for item in options"></rg-select>');
439
440 ctrl.optionsParser.getLabel(scope.options[0]).should.equal(scope.options[0].name);
441 });
442
443 it('Should support description', () => {
444 scope.options = [{key: 1, fullText: 'testname'}];
445
446 compileTemplate('<rg-select options="item.name select as item.fullText for item in options"></rg-select>');
447
448 ctrl.optionsParser.getSelectedLabel(scope.options[0]).should.equal(scope.options[0].fullText);
449 });
450
451 it('Should support selected label customization', () => {
452 scope.options = [{key: 1, fullText: 'testname'}];
453
454 compileTemplate('<rg-select options="item.name select as item.fullText for item in options"></rg-select>');
455
456 ctrl.optionsParser.getSelectedLabel(scope.options[0]).should.equal(scope.options[0].fullText);
457 });
458
459 it('Should pass selected to callback', () => {
460 scope.options = [{key: 1, label: 'test'}];
461 const selectedModel = {originalModel: scope.options[0]};
462 scope.callback = sandbox.spy();
463
464 compileTemplate('<rg-select options="item in options" on-select="callback(selected)"></rg-select>');
465 ctrl.config.onSelect(selectedModel);
466 scope.$digest();
467
468 scope.callback.should.have.been.calledWith(selectedModel);
469 });
470
471 it('Should pass an event to a callback as a second parameter', () => {
472 scope.event = {};
473 scope.onSelect = sandbox.spy();
474 scope.onDeselect = sandbox.spy();
475 scope.onChange = sandbox.spy();
476 scope.options = [{}];
477 const model = {originalModel: scope.options[0]};
478
479 compileTemplate('<rg-select options="item in options" on-change="onChange(selected,event)" on-select="onSelect(selected,event)" on-deselect="onDeselect(deselected,event)"></rg-select>');
480 ctrl.config.onSelect(model, scope.event);
481 ctrl.config.onChange(model, scope.event);
482 ctrl.config.onDeselect(model, scope.event);
483 scope.$digest();
484
485 scope.onSelect.should.have.been.calledWith(model, scope.event);
486 scope.onDeselect.should.have.been.calledWith(model, scope.event);
487 scope.onChange.should.have.been.calledWith(model, scope.event);
488 });
489
490 it('Should take just plain option as label if option is string', () => {
491 ctrl.optionsParser.getLabel('test').should.equal('test');
492 });
493
494 it('Should return no label if option is object and no valid label mapping provided', () => {
495 should.not.exist(ctrl.optionsParser.getLabel({foo: 'bar'}));
496 });
497
498 it('Should support custom property for ng-model', () => {
499 const optionMock = {value: 1, label: 'label'};
500 scope.options = [optionMock];
501 scope.selectedOption = null;
502
503 compileTemplate('<rg-select ng-model="selectedOption" options="item.value as item.label for item in options"></rg-select>');
504 ctrl.config.onChange({originalModel: optionMock});
505
506 scope.selectedOption.should.equal(optionMock.value);
507 });
508
509 it('Should update select if we pass custom ng-model', () => {
510 const optionMock = {value: 1, label: 'label'};
511 scope.options = [optionMock];
512 scope.selectedOption = optionMock.value;
513
514 compileTemplate('<rg-select ng-model="selectedOption" options="item.value as item.label for item in options" lazy="false"></rg-select>');
515
516 ctrl.selectInstance.props.selected.label.should.equal(optionMock.label);
517 });
518
519 it('Should call only once data source function for primitive ng-model', () => {
520 function createOptionMock(value) {
521 return {
522 value,
523 label: `label ${value}`
524 };
525 }
526
527 scope.options = [
528 createOptionMock(1),
529 createOptionMock(2),
530 createOptionMock(3),
531 createOptionMock(4)
532 ];
533
534 scope.selectedOption = scope.options[0];
535 scope.getOptions = sandbox.stub().returns(scope.options);
536
537 compileTemplate('<rg-select ng-model="selectedOption" options="item.value as item.label for item in getOptions()"></rg-select>');
538 ctrl.loadOptionsToSelect('');
539 scope.$digest();
540
541 scope.getOptions.should.been.calledOnce;
542 });
543
544 it('Should correct update select if options have "track by" with "for" expressions without "as"', () => {
545 compileTemplate('<rg-select ng-model="selectedItem" options="item.name for item in items track by item.id"></rg-select>');
546 ctrl.config.onChange({originalModel: fakeItems[0]});
547 scope.$digest();
548
549 scope.selectedItem.should.equal(fakeItems[0]);
550 });
551
552 it('Should update select if list of options load after ng-model', () => {
553
554 /**
555 * In case `lazy=false` rg-select behave like angular select
556 * It will watch options collection on every digest that may slowdown performance
557 */
558 scope.items = null;
559 scope.selectedItem = fakeItems[0].id;
560 compileTemplate('<rg-select ng-model="selectedItem" lazy="false" options="item.id as item.name for item in items track by item.id"></rg-select>');
561 scope.$digest();
562
563 scope.items = angular.copy(fakeItems);
564 scope.$digest();
565
566 ctrl.selectInstance.props.selected.label.should.equal(fakeItems[0].name);
567 });
568
569 it('Should update select if list of options load after ng-model and update existing array not changing reference', () => {
570 scope.items = [];
571 scope.selectedItem = fakeItems[0].id;
572 compileTemplate('<rg-select ng-model="selectedItem" lazy="false" options="item.id as item.name for item in items track by item.id"></rg-select>');
573 scope.$digest();
574
575 scope.items = angular.extend(scope.items, fakeItems);
576 scope.$digest();
577
578 ctrl.selectInstance.props.selected.label.should.equal(fakeItems[0].name);
579 });
580
581 it('Should throw exception if we have two options with same ng-model value', () => {
582 const optionMock = {value: 1, label: 'label'};
583 scope.options = [optionMock, optionMock];
584 scope.selectedOption = optionMock.value;
585
586 function compile() {
587 compileTemplate('<rg-select ng-model="selectedOption" options="item.value as item.label for item in options" lazy="false"></rg-select>');
588 }
589
590 compile.should.throw(Error);
591 });
592
593 it('Should parse option variable name', () => {
594 compileTemplate('<rg-select options="itemvar in items track by itemvar.id"></rg-select>');
595
596 ctrl.optionsParser.optionVariableName.should.be.equal('itemvar');
597 });
598
599 it('should clear last query on close', () => {
600 ctrl.query = 'query';
601 ctrl.config.onClose();
602 scope.$digest();
603
604 should.not.exist(ctrl.query);
605 });
606 });
607
608 function initializeReactSelect(node) {
609 simulateClick(findContainerNode(node));
610 }
611
612 function simulateClick(node) {
613 const clickEvent = new CustomEvent('click');
614 node.dispatchEvent(clickEvent);
615 }
616
617 function findContainerNode(node) {
618 return node.querySelector('span');
619 }
620});