1 |
|
2 |
|
3 | import 'dom4';
|
4 | import React from 'react';
|
5 | import {findDOMNode} from 'react-dom';
|
6 | import {Simulate} from 'react-dom/test-utils';
|
7 | import {mount} from 'enzyme';
|
8 |
|
9 |
|
10 | import simulateCombo from '../../test-helpers/simulate-combo';
|
11 |
|
12 | import QueryAssist from './query-assist';
|
13 | import styles from './query-assist.css';
|
14 |
|
15 | describe('Query Assist', () => {
|
16 | const testQuery = 'oooooooooooo';
|
17 | const testQueryLength = testQuery.length;
|
18 |
|
19 | const suggestions = [{
|
20 | prefix: 'login: ',
|
21 | option: 'test',
|
22 | suffix: ' ',
|
23 | description: 'logins',
|
24 | matchingStart: 0,
|
25 | matchingEnd: 4,
|
26 | caret: 2,
|
27 | completionStart: 0,
|
28 | completionEnd: 4,
|
29 | group: 'logins',
|
30 | icon: 'data:uri'
|
31 | }, {
|
32 | prefix: 'login: ',
|
33 | option: 'test.1',
|
34 | suffix: ' ',
|
35 | description: 'logins',
|
36 | matchingStart: 0,
|
37 | matchingEnd: 4,
|
38 | caret: 2,
|
39 | completionStart: 0,
|
40 | completionEnd: 4,
|
41 | group: 'logins',
|
42 | icon: 'data:uri'
|
43 | }, {
|
44 | prefix: 'login: ',
|
45 | option: 'test.2',
|
46 | suffix: ' ',
|
47 | description: 'logins',
|
48 | matchingStart: 0,
|
49 | matchingEnd: 4,
|
50 | caret: 2,
|
51 | completionStart: 0,
|
52 | completionEnd: 4,
|
53 | group: 'logins',
|
54 | icon: 'data:uri'
|
55 | }, {
|
56 | prefix: 'login: ',
|
57 | option: 'test.3',
|
58 | suffix: ' ',
|
59 | description: 'logins',
|
60 | matchingStart: 0,
|
61 | matchingEnd: 4,
|
62 | caret: 2,
|
63 | completionStart: 0,
|
64 | completionEnd: 4,
|
65 | group: 'logins',
|
66 | icon: 'data:uri'
|
67 | }];
|
68 |
|
69 | const defaultProps = () => ({
|
70 | query: testQuery,
|
71 | focus: true,
|
72 | dataSource: sandbox.spy(({query, caret}) => ({
|
73 | query,
|
74 | caret,
|
75 | suggestions
|
76 | }))
|
77 | });
|
78 | const mountQueryAssist = (props, options) =>
|
79 | mount(<QueryAssist {...defaultProps()} {...props}/>, options);
|
80 |
|
81 | describe('props to state passing', () => {
|
82 | it('should create component', () => {
|
83 | const wrapper = mountQueryAssist();
|
84 |
|
85 | wrapper.should.exist;
|
86 | wrapper.instance().input.should.exist;
|
87 | });
|
88 |
|
89 | it('should set state props to state on init', () => {
|
90 | const wrapper = mountQueryAssist();
|
91 | wrapper.should.have.state('query', testQuery);
|
92 | wrapper.should.have.state('placeholderEnabled', false);
|
93 | });
|
94 |
|
95 | it('should not set other props to state on init', () => {
|
96 | const wrapper = mountQueryAssist();
|
97 |
|
98 | wrapper.should.not.have.state('popupClassName');
|
99 | wrapper.should.not.have.state('dataSource');
|
100 | wrapper.should.not.have.state('disabled');
|
101 | wrapper.should.not.have.state('clear');
|
102 | wrapper.should.not.have.state('hint');
|
103 | wrapper.should.not.have.state('hintOnSelection');
|
104 | wrapper.should.not.have.state('glass');
|
105 | wrapper.should.not.have.state('placeholder');
|
106 | wrapper.should.not.have.state('onApply');
|
107 | wrapper.should.not.have.state('onChange');
|
108 | wrapper.should.not.have.state('onClear');
|
109 | wrapper.should.not.have.state('onFocusChange');
|
110 | });
|
111 |
|
112 | it('should set state props to state on update', () => {
|
113 | const wrapper = mountQueryAssist({
|
114 | query: 'update',
|
115 | caret: 2,
|
116 | focus: false
|
117 | });
|
118 |
|
119 | wrapper.should.have.state('query', 'update');
|
120 | });
|
121 |
|
122 | it('should set state props to immediateState on update', () => {
|
123 | const instance = mountQueryAssist({
|
124 | query: 'update',
|
125 | caret: 2,
|
126 | focus: false
|
127 | }).instance();
|
128 |
|
129 | instance.immediateState.query.should.equal('update');
|
130 | instance.immediateState.caret.should.equal(2);
|
131 | instance.immediateState.focus.should.equal(false);
|
132 | });
|
133 |
|
134 | it('should not set undefined state props to state on update', () => {
|
135 | const wrapper = mountQueryAssist();
|
136 |
|
137 | wrapper.setProps({
|
138 | query: undefined
|
139 | });
|
140 |
|
141 | wrapper.should.have.state('query', testQuery);
|
142 | });
|
143 |
|
144 | it('should not set caret with query on update', () => {
|
145 | const wrapper = mountQueryAssist();
|
146 | const instance = wrapper.instance();
|
147 |
|
148 | wrapper.setProps({
|
149 | query: 'update'
|
150 | });
|
151 |
|
152 | wrapper.should.have.state('query', 'update');
|
153 | instance.immediateState.query.should.equal('update');
|
154 | instance.immediateState.caret.should.equal(testQueryLength);
|
155 | });
|
156 | });
|
157 |
|
158 |
|
159 | describe('setFocus', () => {
|
160 | it('should set focus in query assist', () => {
|
161 | const instance = mountQueryAssist({focus: null}).instance();
|
162 |
|
163 | instance.setFocus(true);
|
164 |
|
165 | instance.immediateState.focus.should.equal(true);
|
166 | });
|
167 |
|
168 | it('should remove focus from query assist', () => {
|
169 | const instance = mountQueryAssist({focus: true}).instance();
|
170 |
|
171 | instance.setFocus(false);
|
172 |
|
173 | instance.immediateState.focus.should.equal(false);
|
174 | });
|
175 | });
|
176 |
|
177 |
|
178 | describe('shortcuts', () => {
|
179 | it('should enable shortcuts when we set focus', () => {
|
180 | const instance = mountQueryAssist({focus: null}).instance();
|
181 | instance.state.shortcuts.should.equal(false);
|
182 |
|
183 | instance.setFocus(true);
|
184 | instance.state.shortcuts.should.equal(true);
|
185 | });
|
186 |
|
187 |
|
188 | it('should disable shortcuts when we remove focus', () => {
|
189 | const instance = mountQueryAssist({focus: true}).instance();
|
190 | instance.state.shortcuts.should.equal(true);
|
191 |
|
192 | instance.setFocus(false);
|
193 | instance.state.shortcuts.should.equal(false);
|
194 | });
|
195 |
|
196 |
|
197 | it('should not enable shortcuts after rerender', () => {
|
198 | const wrapper = mountQueryAssist({focus: false, placeholder: 'bar'});
|
199 | const instance = wrapper.instance();
|
200 | instance.state.shortcuts.should.equal(false);
|
201 |
|
202 | wrapper.setProps({placeholder: 'foo'});
|
203 | instance.state.shortcuts.should.equal(false);
|
204 | });
|
205 | });
|
206 |
|
207 |
|
208 | describe('init', () => {
|
209 | it('requestData should exist', () => {
|
210 | const instance = mountQueryAssist().instance();
|
211 | instance.requestData.should.be.a('function');
|
212 | instance.requestData.should.equal(instance.requestHandler);
|
213 | });
|
214 |
|
215 | it('requestData should be debounced when delay set', () => {
|
216 | const instance = mountQueryAssist({
|
217 | delay: 0
|
218 | }).instance();
|
219 | instance.requestData.should.be.a('function');
|
220 | instance.requestData.should.not.equal(instance.requestHandler);
|
221 | });
|
222 |
|
223 |
|
224 | it('should create popup when autoOpen', done => {
|
225 | mountQueryAssist({
|
226 | autoOpen: true,
|
227 | dataSource: params => {
|
228 | params.should.not.have.property('omitSuggestions');
|
229 | done();
|
230 | }
|
231 | });
|
232 | });
|
233 |
|
234 | it('should not create popup by default', done => {
|
235 | mountQueryAssist({
|
236 | dataSource: params => {
|
237 | params.should.have.property('omitSuggestions', true);
|
238 | done();
|
239 | }
|
240 | });
|
241 | });
|
242 | });
|
243 |
|
244 | describe('rendering', () => {
|
245 | const LETTER_CLASS = styles.letter;
|
246 |
|
247 | it('should render letters', () => {
|
248 | const instance = mountQueryAssist().instance();
|
249 |
|
250 | instance.input.should.contain(`.${LETTER_CLASS}`);
|
251 | instance.input.queryAll(`.${LETTER_CLASS}`).
|
252 | should.have.length(testQueryLength);
|
253 | });
|
254 |
|
255 |
|
256 | it('should render nothing on empty query', () => {
|
257 | const instance = mountQueryAssist({
|
258 | query: ''
|
259 | }).instance();
|
260 |
|
261 | instance.input.textContent.should.be.empty;
|
262 | });
|
263 |
|
264 | it('should render nothing on falsy query', () => {
|
265 | const instance = mountQueryAssist({
|
266 | query: null
|
267 | }).instance();
|
268 |
|
269 | instance.input.textContent.should.be.empty;
|
270 | });
|
271 |
|
272 | it('Shouldnt make duplicate requests for styleRanges on initiating if query is provided', () => {
|
273 | const wrapper = mountQueryAssist();
|
274 |
|
275 | //Emulate multiple rerender when rendering component with react-ng
|
276 | wrapper.setProps({});
|
277 | wrapper.setProps({});
|
278 |
|
279 | wrapper.prop('dataSource').should.have.been.calledOnce;
|
280 | });
|
281 |
|
282 | it('should render placeholder when enabled on empty query', () => {
|
283 | const instance = mountQueryAssist({
|
284 | query: '',
|
285 | placeholder: 'plz'
|
286 | }).instance();
|
287 |
|
288 | instance.placeholder.should.exist;
|
289 | instance.placeholder.should.have.text('plz');
|
290 | });
|
291 |
|
292 | it('should not render placeholder when disabled on empty query', () => {
|
293 | const instance = mountQueryAssist({
|
294 | query: ''
|
295 | }).instance();
|
296 |
|
297 | should.not.exist(instance.placeholder);
|
298 | });
|
299 |
|
300 | it('should render with colors', () => {
|
301 | const wrapper = mountQueryAssist();
|
302 |
|
303 | wrapper.setState({
|
304 | styleRanges: [
|
305 | {start: 0, length: 1, style: 'text'},
|
306 | {start: 1, length: 1, style: 'field_value'},
|
307 | {start: 2, length: 1, style: 'field_name'},
|
308 | {start: 3, length: 1, style: 'operator'}
|
309 | ]
|
310 | });
|
311 |
|
312 | const letters = wrapper.instance().input.queryAll(`.${LETTER_CLASS}`);
|
313 |
|
314 | letters[0].should.have.class(styles['letter-text']);
|
315 | letters[1].should.have.class(styles['letter-field-value']);
|
316 | letters[2].should.have.class(styles['letter-field-name']);
|
317 | letters[3].should.have.class(styles['letter-operator']);
|
318 | });
|
319 |
|
320 | it('should render last text range with default style when applied', () => {
|
321 | const wrapper = mountQueryAssist({
|
322 | query: 'a a'
|
323 | });
|
324 |
|
325 | wrapper.setState({
|
326 | dirty: true,
|
327 | styleRanges: [
|
328 | {start: 0, length: 1, style: 'text'},
|
329 | {start: 2, length: 1, style: 'text'}
|
330 | ]
|
331 | });
|
332 |
|
333 | const letters = wrapper.instance().input.queryAll(`.${LETTER_CLASS}`);
|
334 |
|
335 | letters[0].should.have.class(styles['letter-text']);
|
336 | letters[1].should.have.class(styles.letterDefault);
|
337 | letters[2].should.have.class(styles.letterDefault);
|
338 | });
|
339 |
|
340 | it('should render last text range with text style when applied', () => {
|
341 | const wrapper = mountQueryAssist({
|
342 | query: 'a a'
|
343 | });
|
344 |
|
345 | wrapper.setState({
|
346 | styleRanges: [
|
347 | {start: 0, length: 1, style: 'text'},
|
348 | {start: 2, length: 1, style: 'text'}
|
349 | ]
|
350 | });
|
351 |
|
352 | const letters = wrapper.instance().input.queryAll(`.${LETTER_CLASS}`);
|
353 |
|
354 | letters[0].should.have.class(styles['letter-text']);
|
355 | letters[1].should.have.class(styles.letterDefault);
|
356 | letters[2].should.have.class(styles['letter-text']);
|
357 | });
|
358 |
|
359 | it('should disable field when component disabled', () => {
|
360 | const instance = mountQueryAssist({
|
361 | disabled: true
|
362 | }).instance();
|
363 |
|
364 | instance.input.should.have.attr('contenteditable', 'false');
|
365 | instance.input.should.have.class(styles.inputDisabled);
|
366 | });
|
367 |
|
368 | it('should render glass when enabled', () => {
|
369 | const instance = mountQueryAssist({
|
370 | glass: true
|
371 | }).instance();
|
372 |
|
373 | instance.glass.should.exist;
|
374 | });
|
375 |
|
376 | it('should not render glass when disabled', () => {
|
377 | const instance = mountQueryAssist({
|
378 | glass: false
|
379 | }).instance();
|
380 |
|
381 | should.not.exist(instance.glass);
|
382 | });
|
383 |
|
384 | it('should render clear when enabled', () => {
|
385 | const instance = mountQueryAssist({
|
386 | clear: true
|
387 | }).instance();
|
388 |
|
389 | instance.clear.should.exist;
|
390 | });
|
391 |
|
392 | it('should not render clear when disabled', () => {
|
393 | const instance = mountQueryAssist({
|
394 | clear: false
|
395 | }).instance();
|
396 |
|
397 | should.not.exist(instance.clear);
|
398 | });
|
399 |
|
400 | it('should not render clear when query is empty', () => {
|
401 | const instance = mountQueryAssist({
|
402 | clear: true,
|
403 | query: ''
|
404 | }).instance();
|
405 |
|
406 | should.not.exist(instance.clear);
|
407 | });
|
408 |
|
409 | it('should show loader on long request', () => {
|
410 | const wrapper = mountQueryAssist();
|
411 | wrapper.setState({
|
412 | loading: true
|
413 | });
|
414 |
|
415 | wrapper.instance().loader.should.exist;
|
416 | });
|
417 | });
|
418 |
|
419 | describe('suggestions', () => {
|
420 | it('should not show popup when no suggestions provided', done => {
|
421 | const instance = mountQueryAssist({
|
422 | dataSource: ({query, caret}) => ({
|
423 | query,
|
424 | caret,
|
425 | suggestions: []
|
426 | })
|
427 | }).instance();
|
428 |
|
429 | instance.requestData().
|
430 | then(() => {
|
431 | instance._popup.isVisible().should.be.false;
|
432 | done();
|
433 | });
|
434 | });
|
435 |
|
436 | it('should show popup when suggestions provided', done => {
|
437 | const instance = mountQueryAssist().instance();
|
438 |
|
439 | instance.requestData().
|
440 | then(() => {
|
441 | instance._popup.isVisible().should.be.true;
|
442 | instance._popup.list.should.exist;
|
443 | done();
|
444 | });
|
445 | });
|
446 |
|
447 | it('should close popup with after zero suggestions provided', done => {
|
448 | let currentSuggestions = suggestions;
|
449 | const instance = mountQueryAssist({
|
450 | dataSource: ({query, caret}) => ({
|
451 | query,
|
452 | caret,
|
453 | suggestions: currentSuggestions
|
454 | })
|
455 | }).instance();
|
456 |
|
457 | instance.requestData().
|
458 | then(() => {
|
459 | currentSuggestions = [];
|
460 | instance.requestData().
|
461 | then(() => {
|
462 | instance._popup.isVisible().should.be.false;
|
463 | done();
|
464 | });
|
465 | });
|
466 | });
|
467 |
|
468 | it('should show popup with proper suggestions', done => {
|
469 | const container = document.createElement('div');
|
470 | document.body.appendChild(container);
|
471 | const wrapper = mountQueryAssist({}, {attachTo: container});
|
472 | const instance = wrapper.instance();
|
473 |
|
474 | const TWICE = 2;
|
475 |
|
476 | instance.requestData().
|
477 | then(() => {
|
478 | const list = findDOMNode(instance._popup.list);
|
479 | const {length} = suggestions;
|
480 |
|
481 | list.queryAll('[data-test~=ring-list-item]').should.have.length(length);
|
482 | list.queryAll(`.${styles.highlight}`).should.have.length(length);
|
483 | list.queryAll(`.${styles.service}`).should.have.length(length * TWICE);
|
484 |
|
485 | wrapper.detach();
|
486 | document.body.removeChild(container);
|
487 | done();
|
488 | }).catch(done);
|
489 | });
|
490 |
|
491 | });
|
492 |
|
493 | describe('completion', () => {
|
494 | const completeQuery = 'test';
|
495 | const middleCaret = completeQuery.length / 2;
|
496 |
|
497 | function getSuggestionText({prefix, option, suffix}) {
|
498 | return (prefix + option + suffix).replace(/\s/g, '\u00a0');
|
499 | }
|
500 |
|
501 | it('should complete by tab in the end of phrase', () => {
|
502 | const instance = mountQueryAssist({
|
503 | query: completeQuery
|
504 | }).instance();
|
505 |
|
506 | return instance.requestData().then(() => {
|
507 | simulateCombo('tab');
|
508 |
|
509 | instance.input.should.have.text(getSuggestionText(suggestions[0]));
|
510 | });
|
511 | });
|
512 |
|
513 | it('should complete selected suggestion by enter in the end of phrase', () => {
|
514 | const instance = mountQueryAssist({
|
515 | query: completeQuery
|
516 | }).instance();
|
517 |
|
518 | return instance.requestData().then(() => {
|
519 | simulateCombo('down enter');
|
520 |
|
521 | instance.input.should.have.text(getSuggestionText(suggestions[0]));
|
522 | });
|
523 | });
|
524 |
|
525 | it('should complete by tab in the middle of phrase', () => {
|
526 | const instance = mountQueryAssist({
|
527 | query: completeQuery,
|
528 | caret: middleCaret
|
529 | }).instance();
|
530 |
|
531 | return instance.requestData().then(() => {
|
532 | simulateCombo('tab');
|
533 |
|
534 | instance.input.should.have.text(getSuggestionText(suggestions[0]));
|
535 | });
|
536 | });
|
537 |
|
538 | it('should complete selected suggestion by enter in the middle of phrase', () => {
|
539 | const instance = mountQueryAssist({
|
540 | query: completeQuery,
|
541 | caret: middleCaret
|
542 | }).instance();
|
543 |
|
544 | return instance.requestData().then(() => {
|
545 | simulateCombo('down enter');
|
546 |
|
547 | instance.input.should.
|
548 | have.text(getSuggestionText(suggestions[0]) + completeQuery.substring(middleCaret));
|
549 | });
|
550 | });
|
551 |
|
552 | it('should complete selected suggestion by tab in the middle of phrase', () => {
|
553 | const instance = mountQueryAssist({
|
554 | query: completeQuery,
|
555 | caret: middleCaret
|
556 | }).instance();
|
557 |
|
558 | return instance.requestData().then(() => {
|
559 | simulateCombo('down down down tab');
|
560 |
|
561 | instance.input.should.have.text(getSuggestionText(suggestions[2]));
|
562 | });
|
563 | });
|
564 | });
|
565 |
|
566 | describe('callbacks', () => {
|
567 | let onApply;
|
568 | beforeEach(() => {
|
569 | onApply = sandbox.stub();
|
570 | });
|
571 |
|
572 | it('should call onApply', () => {
|
573 | mountQueryAssist({
|
574 | onApply
|
575 | });
|
576 |
|
577 | simulateCombo('enter');
|
578 | onApply.should.have.been.calledWithMatch({
|
579 | query: testQuery,
|
580 | caret: testQueryLength
|
581 | });
|
582 | });
|
583 |
|
584 | it('should call onApply when press ctrl/cmd + enter', () => {
|
585 | mountQueryAssist({
|
586 | onApply
|
587 | });
|
588 |
|
589 | simulateCombo('ctrl+enter');
|
590 | onApply.should.have.been.calledWithMatch({
|
591 | query: testQuery,
|
592 | caret: testQueryLength
|
593 | });
|
594 | });
|
595 |
|
596 | it('should call onApply from glass', () => {
|
597 | const instance = mountQueryAssist({
|
598 | glass: true,
|
599 | onApply
|
600 | }).instance();
|
601 |
|
602 | Simulate.click(findDOMNode(instance.glass));
|
603 | onApply.should.have.been.calledWithMatch({
|
604 | query: testQuery,
|
605 | caret: testQueryLength
|
606 | });
|
607 | });
|
608 |
|
609 | it('should call onClear', () => {
|
610 | const onClear = sandbox.stub();
|
611 | const instance = mountQueryAssist({
|
612 | clear: true,
|
613 | onClear
|
614 | }).instance();
|
615 |
|
616 | Simulate.click(findDOMNode(instance.clear));
|
617 | onClear.should.have.been.calledWithExactly();
|
618 | });
|
619 | });
|
620 |
|
621 | describe('request data', () => {
|
622 | it('should batch requests', () => {
|
623 | sandbox.useFakeTimers({toFake: ['setTimeout', 'clearTimeout']});
|
624 |
|
625 | const wrapper = mountQueryAssist();
|
626 | const instance = wrapper.instance();
|
627 | wrapper.setProps({
|
628 | delay: 100
|
629 | }, () => {
|
630 | wrapper.prop('dataSource').resetHistory();
|
631 |
|
632 | instance.requestData();
|
633 | instance.requestData();
|
634 | instance.requestData();
|
635 | sandbox.clock.tick(400);
|
636 |
|
637 | wrapper.prop('dataSource').should.have.been.calledOnce;
|
638 | });
|
639 |
|
640 | });
|
641 | });
|
642 |
|
643 |
|
644 | describe('custom actions', () => {
|
645 | it('should allow to pass custom actions', () => {
|
646 | const wrapper = mountQueryAssist({
|
647 | actions: [
|
648 | <div id={'A'} key={'A'}/>,
|
649 | <div id={'B'} key={'B'}/>
|
650 | ]
|
651 | });
|
652 |
|
653 | wrapper.find('#A').should.exist;
|
654 | wrapper.find('#B').should.exist;
|
655 | });
|
656 | });
|
657 | });
|