UNPKG

vsm-autocomplete

Version:

Vue-component for term+ID lookup based on a vsm-dictionary

1,478 lines (1,167 loc) 106 kB
import VsmAutocomplete from './VsmAutocomplete.vue'; import VsmDictionaryLocal from 'vsm-dictionary-local'; // Some extra, global variables are defined in 'test-setup.js'. const Z = false; // Disable some subtests while awaiting vue-test-utils bugfixes. describe('VsmAutocomplete', () => { // We first create a sizable test-setup, after which we run the actual tests. // --- 1/5) CREATE A TEST-VsmDictionary --- // DictInfo/entry/refTerm-data, for a test-VsmDictionaryLocal. const di1 = { id: 'A', name: 'Name 1' }; const di2 = { id: 'B', name: 'Name 2' }; const di3 = { id: 'C', name: 'Name 3' }; const e1 = { id: 'A:01', dictID: 'A', terms: [{ str: 'a' }] }; const e2 = { id: 'A:02', dictID: 'A', terms: [{ str: 'ab' }] }; const e3 = { id: 'B:01', dictID: 'B', terms: [{ str: 'bc' }] }; const e4 = { id: 'B:02', dictID: 'B', terms: [{ str: 'bcd' }, { str: 'x' }], descr: 'Descr' }; const e5 = { id: 'B:03', dictID: 'B', terms: [{ str: 'bcc' }] }; const e6 = { id: 'C:01', dictID: 'C', terms: [{ str: 'xyz' }] }; const r1 = 'it'; const qoFT = {idts: [{id: 'B:03'}]}; // `queryOptions` to incl e5 as fixedTerm. // This combines a dictInfo with entries, like VD.Local's `dictData` wants it. const addEntries = (di, ...entries) => Object.assign({}, di, { entries }); function makeDictionary(options = {}) { return new VsmDictionaryLocal(Object.assign({}, options, { dictData: [ addEntries(di1, e1, e2), addEntries(di2, e3, e4, e5), addEntries(di3, e6) ], refTerms: [r1] })); } /** * + We create one VsmDictionary for all tests, now. * + By using a VsmDictionaryLocal with a delay, we'll e.g. be able to know if * a request was made, by seeing that TheList doesn't open until after the * req./s's delay/s has/ve passed. If no immediate list-open, it made a req. * + Note that VsmAutocomplete may query all of: loadFixedTerms(), * getMatchesForString(), and getDictInfos(). So TheList may open after * three times the `delay`, if the component needed to make all 3 queries. * + Note: in some examples we will us `e5` a 'fixedTerm'. We do not need to * call `dict.loadFixedTerms([{ id: 'B:03' }], ..)` here, because VsmAutoC.. * will call it, when it is constructed with `queryOptions.idts` = `qoFT`. */ var dict = makeDictionary({ delay: 100 }); // --- 2/5) FOR CREATING A TEST-VsmAutocomplete-COMPONENT --- /** * This is used by each test to create a test-component with custom props. * By using `mount()` (not `shallowMount`), it adds full subcomponents too. */ const make = (props, listeners) => mount(VsmAutocomplete, { propsData: Object.assign( { vsmDictionary: dict }, // Add at least the required prop (overridable). props // Insert test-specific props from the arg. ), listeners: listeners || { // Add a set of listeners, by default. 'item-literal-select': s => s, ///D(`called with ${s}.`), 'item-active-change': o => o ///D(o) } }); // --- 3/5) UTILITY FUNCTIONALITY --- // `w` is an IMPORTANT variable in all the tests! // ALL tests will `make()` a component 'wrapper', which the `_...()` functions // below can access. A global `w` frees us from giving it as arg. at each call. var w; // Shorthand functions for repeatedly accessed HTML parts. // + NOTE: We test only the effect of settings (props) & user-actions, // on the generated HTML & emitted events. // It is good to avoid testing internal state variables (`.vm.*`), // because these may change along with the implementation. const _input = () => w.find('.input'); const _inputV = () => _input().element.value; const _inputA = () => _input().attributes(); const _placeh = () => w.find('.placehold'); const _list = () => w.find('.list'); const _listEx = () => _list().exists(); const _items = () => w.findAll('.list .item'); const _item = index => w.findAll('.list .item').at(index); const _itemPS = index => _item(index).find('.item-part-str' ); const _itemPD = index => _item(index).find('.item-part-descr'); const _itemPI = index => _item(index).find('.item-part-info' ); const _itemPST = index => _itemPS(index).text(); const _itemPDT = index => _itemPD(index).text(); const _itemL = () => w.find('.list .item-type-literal'); const _itemLT = () => _itemL().text(); const _spinner = () => w.find('.spinner'); // Shorthand conditions. const _listLen = () => // ->Nr of TheList-items, or false if list is closed. !_listEx() ? false : _items().length; const _itemLOnly = () => // True if open TheList, with only ListItemLiteral. _listLen() == 1 && _itemL().exists(); // Shorthand functions to trigger user-generated events. const _focus = () => _input().trigger('focus'); const _blur = () => _input().trigger('blur'); const _keyUp = () => _input().trigger('keydown.up'); const _keyDown = () => _input().trigger('keydown.down'); const _keyEsc = () => _input().trigger('keydown.esc'); const _keyEnter = () => _input().trigger('keydown.enter'); const _keyBksp = () => _input().trigger('keydown.backspace'); const _keyTab = () => _input().trigger('keydown.tab'); const _keySTab = () => _input().trigger('keydown.tab', { shiftKey: true }); const _keyCEnter = () => _input().trigger('keydown.enter', { ctrlKey: true }); const _keySEnter = () => _input().trigger('keydown.enter', { shiftKey: true }); const _setInput = newValue => { // Changes the content of TheInput. var input = _input(); input.element.value = newValue; input.trigger('input'); }; // Shorthand functions to check if a certain event was emitted X times. // Returns true/false if `str` was emitted at least `index+1` times. const _emit = (index = 0, str) => { var emit = w.emitted(str); return emit !== undefined && emit[index] !== undefined; }; // Returns 0 if `str` emitted <`index+1` times; else `index`'th emitted value. const _emitV = (index = 0, str) => { var emit = w.emitted(str); return emit !== undefined && emit[index] !== undefined ? emit[index][0] : 0; }; const _emitIAC = index => _emitV(index, 'item-active-change'); const _emitIC = index => _emitV(index, 'input-change'); const _emitI = index => _emitV(index, 'input'); const _emitSel = index => _emitV(index, 'item-select'); const _emitLSel = index => _emitV(index, 'item-literal-select'); const _emitLO = index => _emit (index, 'list-open'); const _emitLC = index => _emit (index, 'list-close'); const _emitF = index => _emit (index, 'focus'); const _emitB = index => _emit (index, 'blur'); const _emitEsc = index => _emit (index, 'key-esc'); const _emitBksp = index => _emit (index, 'key-bksp'); const _emitCEnt = index => _emit (index, 'key-ctrl-enter'); const _emitSEnt = index => _emit (index, 'key-shift-enter'); const _emitTab = index => _emitV(index, 'key-tab'); const _emitDblc = index => _emit (index, 'dblclick'); const _emitOvIn = index => _emit (index, 'mouseover-input'); // Shorthand test functions. const _onlyOneItemIsActive = () => w.findAll('.list .item.item-state-active').length.should.equal(1); const _itemXIsActive = index => _item(index).classes().should.contain('item-state-active'); // Shorthand that makes `Vue.nextTick` just a bit more concise. const vueTick = cb => Vue.nextTick(cb); // Shorthand for inspecting emitted events, while debugging. const DE = () => D(w.emittedByOrder()); // eslint-disable-line no-unused-vars // --- 4/5) TIME TRAVEL SETUP --- /* - When we mount() a test-component, it launches one or more requests for data to its VsmDictionary; and we need these requests to finish before we let the tests assert things. This takes some 'time'/event-loops. - But we can not use `Vue.nextTick()` here, because that only makes Vue's DOM-updates finish, not requests to external datasources. + So `Vue.nextTick(() => { (1).should.equal(1); cb(); });` does not work. (Not even with several nested nextTick-calls). - It would work with `setTimeout()`. And since we use VSMDictionaryLocal, we know that there will be no large delay; only a few event-loop jumps). But this approach will slow down our tests. + So `setTimeout(() => { (1).should.equal(1); cb(); }, 10);` should not be used. - Instead, we use 'sinon' to artificially move forward time. - See https://stackoverflow.com/questions/17446064 about 'useFakeTimers()`. - We initialize it in `beforeEach/afterEach()`, even though we may not need fakeTimers every time. But this ensures that `clock.restore()` is always called, even when a test fails (which would prevent it from making the `restore()` call by itself at the end, and that would affect next tests). + This also enables us to make efficient tests, based on custom VsmDictionary- delays, as described earlier. */ var clock; beforeEach(() => { clock = sinon.useFakeTimers(); }); afterEach(() => { clock.restore(); }); // --- 5/5) THE TESTS --- describe('handles props on a VsmAutocomplete component', () => { it('initializes, when getting only the required props', () => { // For this first test, we show how `mount/shallowMount()` is really used. // But in all next tests, we'll use more concise code, by using `make()`. w = mount(VsmAutocomplete, { // We assign it to our global `w` test-var. propsData: { vsmDictionary: dict /// autofocus: false, // Default, not-required values. These will.. /// placeholder: false, // ..be changed in some of the other tests. /// initialValue: '', /// queryOptions: { perPage: 20 }, /// maxStringLengths: { str: 40, strAndDescr: 70 }, /// customItemLiteral: false, /// customItem: false } }); ///H(w); w.isVueInstance().should.equal(true); w.classes().should.contain('vsm-autocomplete'); // Note: _input () === w.find('.input') ; // _inputA() === w.find('.input').attributes() ; // _inputV() === w.find('.input').element.value ; etc. expect(_inputA().autofocus).to.equal(undefined); _inputV().should.equal(''); _input ().classes().should.not.contain('error'); _placeh().exists().should.equal(false); _list ().exists().should.equal(false); _spinner().exists().should.equal(false); _items ().exists().should.equal(false); }); it('uses the `autofocus` prop', () => { w = make({ autofocus: false }); expect(_inputA().autofocus).to.equal(undefined); w = make({ autofocus: true }); _inputA().autofocus.should.not.equal(undefined); }); /* // The 'autofocus' attribute does not have a UI-effect in 'vue-test-utils', // so we can't test its effect. it('when `autofocus` is set, and TheInput is empty, and ' + 'fixedTerm-matches exist: then opens TheList immediately', () => { w = make({ autofocus: true, queryOptions: { idts: [{ id: 'D:002' }] } }); clock.tick(300); _listEx().should.equal(true); H(w); }); */ it('uses the `placeholder` prop', () => { w = make({ placeholder: false }); _placeh().exists().should.equal(false); w = make({ placeholder: 'plc' }); _placeh().text().should.equal('plc'); _inputV() .should.equal(''); // TheInput should stay empty. }); it('queries a match-obj. & dependent data for an `initialValue`, ' + 'and shows this all in a ListItem in TheList', () => { w = make( { initialValue: 'ab', // This will match entry `e2`. queryOptions: qoFT }, // Let the queries consider fixedTerms too. {} // This removes all listeners => it makes no item-literal. ); // Focusing TheInput means that the user will soon see a query result. _focus(); _spinner().exists().should.equal(true); clock.tick(100); _list ().exists().should.equal(false); // Now, only loaded fixedTerms. clock.tick(100); _list ().exists().should.equal(false); // Now, still needs dictInfos. // After 300 ms, it loaded fixedTerms, the match-objs, and their dictInfos. clock.tick(100); _input ().classes().should.not.contain('error'); _list ().exists().should.equal(true); _items ().exists().should.equal(true); _items ().length .should.equal(1); _item (0).exists().should.equal(true); _spinner().exists().should.equal(false); _itemPST(0).should.equal('ab'); var title = _itemPI(0).attributes().title; title.should.contain(e2.id); title.should.contain(di1.name); }); it('does not query or open TheList, if given an `initialValue` but no ' + 'focus event; and then only queries after focus', () => { w = make({ initialValue: 'ab' }); // Part 1: TheList does not open without `_focus()`. clock.tick(1000); _listEx().should.equal(false); // Part 2: TheList opens after 'focus', but only after query results took // their time to arrive; i.e. a query still had to be made upon focus. _focus(); clock.tick(150); _listEx().should.equal(false); clock.tick(50); _listEx().should.equal(true); }); it('changes TheInput if the `initialValue` prop is changed', () => { w = make({ initialValue: 'ab' }); _focus(); clock.tick(300); _listEx().should.equal(true); _itemPST(0).should.equal('ab'); w.setProps({ initialValue: 'b' }); _inputV().should.equal('b'); clock.tick(200); _itemPST(0).should.equal('bc'); }); it('does not open a closed TheList if the `initialValue` prop ' + 'is changed', () => { w = make({ initialValue: 'ab' }); clock.tick(300); _listEx().should.equal(false); w.setProps({ initialValue: 'b' }); clock.tick(1000); _listEx().should.equal(false); }); it('does not open an Esc-closed TheList if the `initialValue` prop ' + 'is changed', () => { w = make({ initialValue: 'ab' }); _focus(); clock.tick(300); _listEx().should.equal(true); _keyEsc(); _listEx().should.equal(false); w.setProps({ initialValue: 'b' }); clock.tick(1000); _listEx().should.equal(false); }); it('does not open a closed TheList if the `initialValue` prop ' + 'is changed to the same value as the current TheInput content', () => { w = make({ initialValue: 'ab' }); _focus(); clock.tick(300); _listEx().should.equal(true); _setInput('b'); clock.tick(300); _listEx().should.equal(true); _keyEsc(); _listEx().should.equal(false); w.setProps({ initialValue: 'b' }); clock.tick(1000); _listEx().should.equal(false); }); it('adds a ListItemLiteral if a listener for it is attached', cb => { w = make({ initialValue: 'ab' // Matches `e2`. // - No argument 2: by letting `make()` add its default listeners, // a listener is added for 'item-literal-select', and that causes // VsmAutocomplete to add a ListItemLiteral to TheList. }); _focus(); clock.tick(300); Vue.nextTick(() => { // For some reason, now we need nextTick() here too. /// H(_item(0)); H(_itemL()); _itemPST(0).should.equal ('ab'); _itemLT ( ).should.contain('ab'); cb(); }); }); it('lets prop `customItem` modify the content of a ListItem', cb => { w = make({ initialValue: 'x', // Matches both `e4` and `e6`. maxStringLengths: { str: 50 }, queryOptions: { perPage: 5 }, customItem: data => // Updates 'item-part-str' only for dictID=='C'. data.item.dictID != 'C' ? data.strs : Object.assign(data.strs, { str: `---${data.item.id}-${data.maxStringLengths.str}` + `---${data.queryOptions.perPage}-${data.searchStr}` + `---${data.dictInfo.id}-${data.strs.str}---` }) }); _focus(); clock.tick(300); vueTick(() => { _listLen() .should.equal(3); _itemPST(0).should.equal('x'); _itemPST(1).should.equal('---C:01-50---5-x---C-xyz---'); cb(); }); }); it('only queries for entries\'s `z`-object if a `customItem()` prop ' + 'is given (which is the only function that uses the z-obj.)', cb => { var optFT = 0; var optGM = 0; var dict2 = makeDictionary({ delay: 100 }); dict2.loadFixedTerms = (idts, opt, cb) => { optFT = opt; cb('err') }; dict2.getMatchesForString = (str, opt, cb) => { optGM = opt; cb('err') }; // Part 1: test without a customItem => it adds `.z=[]` to `queryOptions`. w = make({ initialValue: 'x', vsmDictionary: dict2, queryOptions: { idts: [{ id: 'B:03' }] } }); _focus(); clock.tick(300); vueTick(() => { optFT.should.deep.equal({ idts: [{ id: 'B:03' }], page: 1, z: [] }); optGM.should.deep.equal(optFT); // Part 2: test with a `customItem()` => it uses original `queryOptions`. w = make({ initialValue: 'x', vsmDictionary: dict2, queryOptions: { idts: [{ id: 'B:03' }] }, customItem: data => data.strs }); _focus(); clock.tick(300); vueTick(() => { optFT.should.deep.equal({ idts: [{ id: 'B:03' }], page:1 }); // No `z`. optGM.should.deep.equal(optFT); cb(); }); }); }); it('resets the component when the `VsmDictionary` prop is changed', cb => { w = make({ initialValue: 'a' }); _focus(); clock.tick(300); vueTick(() => { _listEx().should.equal(true); var dict2 = makeDictionary({ delay: 100 }); w.setProps({ vsmDictionary: dict2 }); _listEx() .should.equal(false); clock.tick(300); vueTick(() => { _listEx().should.equal(true); cb(); }); }); }); it('lets the `customItemLiteral` prop make custom content for a ' + 'ListItemLiteral', cb => { w = make({ initialValue: 'Q', // Matches no entries. customItemLiteral: data => { data.strs.str = `<div x="y">_-${ data.strs.str }-_</div>`; return data.strs; } }); _focus(); clock.tick(300); vueTick(() => { _itemLOnly().should.equal(true); _itemL().html().should.contain('><div x="y">_-Q-_</div><'); cb(); }); }); it('follows through immediately when `customItemLiteral` prop is ' + 'changed', cb => { w = make({ initialValue: 'Q', customItemLiteral: data => { data.strs.str = `<div x="y">_-${ data.strs.str }-_</div>`; return data.strs; } }); _focus(); clock.tick(300); vueTick(() => { w.setProps({ customItemLiteral: data => { data.strs.str = `**${ data.strs.str }**`; return data.strs; }}); vueTick(() => { _itemLOnly().should.equal(true); _itemL().html().should.contain('>**Q**<'); cb(); }); }); }); it('does not signal an error from `loadFixedTerms()` in TheInput, ' + '(as fixedTerms will then still be available as normal matches)', () => { var dict2 = makeDictionary({ delay: 100 }); dict2.loadFixedTerms = (idts, options, cb) => cb('ERR'); w = make({ initialValue: 'ab', vsmDictionary: dict2 }); clock.tick(300); _input().classes().should.not.contain('error'); }); it('signals an error from `getMatchesForString()` in TheInput; ' + 'and removes it after a next successful request', () => { var dict2 = makeDictionary({ delay: 100 }); var callNr = 0; dict2.getEntryMatchesForString = (searchStr, options, cb) => { ///DEBUG//D('----- QUERY for ' + searchStr); return ++callNr == 1 ? setTimeout(() => cb('ERR'), 0) : dict.getEntryMatchesForString(searchStr, options, cb); }; w = make({ initialValue: 'ab', vsmDictionary: dict2 }); _focus(); clock.tick(300); _input().classes().should.contain('error'); // --- The line below SHOULD be tested. But somehow it fails in this test, // while it succeeds when run live in the browser!... // It fails also with Vue.nextTick(). Maybe a bug in 'vue-test-utils'? // (Maybe this gets fixed in Vue 2.6 & next vue-test-utils?) // => So, we just comment this (& 3 others below) out for now... --- // D(w.vm.isSpinnerShown); // => false // D(_spinner().exists()); // => true, even after nextTick(): Wrong!... if (Z) _spinner().exists().should.equal(false); _setInput('a'); clock.tick(200); _input().classes().should.not.contain('error'); if (Z) _spinner().exists().should.equal(false); }); it('signals an error from `getDictInfos()` in TheInput; ' + 'and removes it after a next successful request', () => { var dict2 = makeDictionary({ delay: 100 }); var callNr = 0; dict2.getDictInfos = (options, cb) => { return ++callNr == 1 ? setTimeout(() => cb('ERR'), 0) : dict.getDictInfos(options, cb); }; w = make({ initialValue: 'ab', vsmDictionary: dict2 }); _focus(); clock.tick(300); _input().classes().should.contain('error'); if (Z) _spinner().exists().should.equal(false); _setInput('a'); clock.tick(200); _input().classes().should.not.contain('error'); if (Z) _spinner().exists().should.equal(false); }); it('shows fixedTerms (given via `queryOptions.idts`), for an empty ' + 'search string', () => { w = make({ initialValue: '', queryOptions: { idts: [{ id: 'B:03' }] } // This also causes an initial.. }); // ..call to `loadFixTerms()` to preload data for this. _focus(); clock.tick(300); _listEx () .should.equal(true); _itemPST(0).should.equal('bcc'); // ListItem for the fixedTerm. }); it('pre-loads new fixedTerms when its prop `queryOptions`(/`.idts`) ' + 'changes; and uses only those in subsequent query results', () => { var dict2 = makeDictionary({ delay: 100 }); w = make({ queryOptions: { idts: [{ id: 'B:03' }] }, // -> `e5`. vsmDictionary: dict2 }); _focus(); clock.tick(100); Object.keys(dict2.fixedTermsCache).length.should.equal(1); clock.tick(200); _listEx () .should.equal(true); _itemPST(0).should.equal('bcc'); w.setProps({ queryOptions: { idts: [{ id: 'B:02' }] } }); // -> `e4`. _listEx () .should.equal(false); clock.tick(100); Object.keys(dict2.fixedTermsCache).length.should.equal(2); clock.tick(200); _listEx () .should.equal(true); _itemPST(0).should.equal('bcd'); }); it('uses `queryOptions.perPage`', () => { w = make( { initialValue: 'b', queryOptions: { perPage: 100 }}, {} // Don't add any listeners => it won't add a ListItemLiteral. ); _focus(); clock.tick(300); _listLen().should.equal(4); w = make( { initialValue: 'b', queryOptions: { perPage: 2 }}, {} ); _focus(); clock.tick(300); _listLen().should.equal(2); }); it('overrides `queryOptions.page` with 1', () => { w = make({ initialValue: 'b', queryOptions: { perPage: 1, page: 1 }}, {}); _focus(); clock.tick(300); _itemPST(0).should.equal('bc'); w = make({ initialValue: 'b', queryOptions: { perPage: 1, page: 2 }}, {}); _focus(); clock.tick(300); _itemPST(0).should.equal('bc'); // Not 'bcc'. So it queried page 1, OK. }); it('uses `maxStringLengths`', () => { w = make({ initialValue: 'bcd' }); // Matches `e4`. _focus(); clock.tick(300); _itemPST(0).should.equal('bcd'); _itemPDT(0).should.equal('(Descr)'); w = make({ initialValue: 'bcd', maxStringLengths: { str: 2 } }); _focus(); clock.tick(300); _itemPST(0).should.equal('b…'); _itemPDT(0).should.equal('(Descr)'); w = make({ initialValue: 'bcd', maxStringLengths: { strAndDescr: 6 } }); _focus(); clock.tick(300); _itemPST( 0).should.equal('bcd'); _itemPDT( 0).should.equal('(De…)'); w = make({ initialValue: 'bcd', maxStringLengths: { str: 2, strAndDescr: 5 } }); _focus(); clock.tick(300); _itemPST(0).should.equal('b…'); _itemPDT(0).should.equal('(De…)'); }); it('follows through immediately when `maxStringLengths` prop is ' + 'changed', () => { w = make({ initialValue: 'bcd' }); // Matches `e4`. _focus(); clock.tick(300); _itemPST(0).should.equal('bcd'); _itemPDT(0).should.equal('(Descr)'); w.setProps({ maxStringLengths: { str: 2 } }); _itemPST(0).should.equal('b…'); _itemPDT(0).should.equal('(Descr)'); }); it('only adds a ListItemLiteral when an \'item-literal-select\' ' + 'listener is added', cb => { // Note: our test-setup adds an 'item-literal-select' listener by default. w = make({ initialValue: 'Q' }); // Matches no entries. _focus(); clock.tick(300); vueTick(() => { _listLen() .should.equal(1); _itemLOnly().should.equal(true); // Subtest 2: don't add any listeners => it won't add a ListItemLiteral. w = make({ initialValue: 'Q' }, {}); _focus(); clock.tick(300); vueTick(() => { _listLen().should.equal(false); // TheList is now closed. cb(); }); }); }); it('only emits an \'item-active-change\' event when a listener for it is ' + 'added', () => { // Note: our test-setup adds an 'item-active-change' listener by default. w = make({ initialValue: 'ab' }); // Matches `e2`. _focus(); clock.tick(300); _item(0).classes().should.contain('item-state-active'); // (First emit's first arg should be `e2`). // w.emitted('item-active-change')[0][0].id.should.equal('A:02'); _emitIAC().id.should.equal('A:02'); // This is shorthand for line above. // Subtest 2: don't add any listeners. w = make({ initialValue: 'ab' }, {}); _focus(); clock.tick(300); _item(0).classes().should.contain('item-state-active'); // expect(w.emitted('item-active-change')).to.equal(undefined); // ..: _emitIAC().should.equal(0); // This is shorthand for line above. }); it('ignores Enter if TheList has been opened less than prop ' + '`freshListDelay` ms ago ', cb => { w = make({ initialValue: 'ab', queryOptions: qoFT, freshListDelay: 1000 }); _focus(); clock.tick(300); _listEx().should.equal(true); _keyEnter(); _listEx().should.equal(true); clock.tick(999); _keyEnter(); _listEx().should.equal(true); _emitSel().should.equal(0); clock.tick(1); _keyEnter(); _listEx().should.equal(false); _emitSel().id.should.deep.equal(e2.id); cb(); }); }); describe('handles user actions on TheInput', () => { it('if changing TheInput\'s content (1): opens a closed TheList, ' + 'and emits \'list-open\' & \'item-active-change\'+matchObj; ' + 'and shows fixedTerms & normal matches', cb => { w = make({ queryOptions: qoFT }); // Makes it consider a fixedTerm. clock.tick(1000); _listEx().should.equal(false); // TheList is closed at start. _setInput('b'); // Change content of TheInput. clock.tick(200); // Wait for results to come in. vueTick(() => { _listEx () .should.equal(true); // After that, TheList is open. _emitLO () .should.equal(true); // 'list-open' was emitted. _emitIAC().id.should.equal('B:03'); // 'item-active-change': the fixedT. _listLen( ).should.equal(5); _item (0).classes().should.contain('item-type-fixed'); // fixedTerm. _itemPST(0).should.equal('bcc'); _itemPST(1).should.equal('bc'); _itemPST(2).should.equal('bcd'); _itemPST(3).should.equal('ab'); _item (4).classes().should.contain('item-type-literal'); cb(); }); }); it('if changing TheInput\'s content (2): opens a closed TheList, ' + 'and emits \'list-open\' & \'item-active-change\'+string ' + 'if this just activated the ListItemLiteral', cb => { w = make({ queryOptions: qoFT }); clock.tick(1000); _listEx().should.equal(false); _setInput('Q'); // Change content of TheInput to match no items. clock.tick(200); vueTick(() => { _listEx ().should.equal(true); _emitLO ().should.equal(true); _emitIAC().should.equal('Q'); // 'item-active-change': the searchStr. _itemLOnly().should.equal(true); cb(); }); }); it('if changing TheInput\'s content (3): changes an open TheList, ' + 'and emits only \'item-active-change\'+matchObj', cb => { w = make({ queryOptions: qoFT }); _setInput('b'); // First, set this value, which will open TheList. clock.tick(300); _listEx().should.equal(true); _emitLO().should.equal(true); // Emitted a first 'list-open'. _setInput('ab'); // Next, make TheInput match `e2` only. clock.tick(300); vueTick(() => { _listEx () .should.equal(true); _emitLO (1).should.equal(false); // Emitted no second 'list-open'. _emitIAC(1).id.should.equal('A:02'); // 'item-active-change' #2: `e2`. _listLen() .should.equal(2); _itemPST(0).should.equal('ab'); _item (1).classes().should.contain('item-type-literal'); cb(); }); }); it('if changing TheInput\'s content (4): changes an open TheList, ' + 'and emits \'item-active-change\'+string if this just activated ' + 'the ListItemLiteral', cb => { w = make({ queryOptions: qoFT }); _setInput('b'); clock.tick(300); vueTick(() => { _setInput('Q'); // Make TheInput match no items. clock.tick(300); vueTick(() => { _listEx () .should.equal(true); _emitLO (1).should.equal(false); // Emitted no second 'list-open'. _emitIAC(1).should.equal('Q'); // 'item-active-change' #2: searchStr. _listLen() .should.equal(1); _item (0).classes().should.contain('item-type-literal'); cb(); }); }); }); it('if changing TheInput\'s content (5): changes an open TheList, and ' + 'and emits nothing if the ListItem actually did not change', cb => { w = make({ queryOptions: qoFT }); _setInput('b'); clock.tick(300); _listEx().should.equal(true); _emitLO().should.equal(true); _setInput('bc'); // This will keep the fixedTerm as a first match. clock.tick(300); vueTick(() => { _listEx () .should.equal(true); _emitLO (1).should.equal(false); // Emitted no second 'list-open'. _emitIAC(1).should.equal(0); // And no 2nd'item-active-change'. _listLen() .should.equal(4); // I.e. 3 matches + ListItemLiteral. _itemPST(0).should.equal('bcc'); cb(); }); }); it('if changing TheInput\'s content (6): changes an open TheList, and ' + 'emits if the only item, ListItemLiteral, changed', cb => { w = make({ queryOptions: qoFT }); _setInput('Q'); clock.tick(300); _listEx().should.equal(true); _emitLO().should.equal(true); _setInput('QQ'); // Results in another single ListItemLiteral. clock.tick(300); vueTick(() => { _listEx () .should.equal(true); _emitLO (1).should.equal(false); // Emitted no second 'list-open'. _emitIAC(1).should.equal('QQ'); // 'item-active-change': emit #2. _listLen() .should.equal(1); _item (0).classes().should.contain('item-type-literal'); cb(); }); }); it('if changing TheInput\'s content (7), and no results: ' + 'closes an open TheList, and emits \'close-list\' & ' + '\'item-active-change\'+false', cb => { w = make( { queryOptions: qoFT }, { 'item-active-change': o => o // No 'item-literal-select' listener => it won't add a ListItemLiteral. } ); _setInput('b'); clock.tick(300); _listEx().should.equal(true); // TheList is open. _emitLO().should.equal(true); _setInput('Q'); // Results in no matches, not even a ListItemLiteral. clock.tick(300); vueTick(() => { _listEx () .should.equal(false); // TheList is closed. _emitLC () .should.equal(true); // It emitted a 'list-close'. _emitIAC(1).should.equal(false); // 'item-active-change' #2: `false`. cb(); }); }); it('if changing TheInput\'s content (8), and no results: leaves a ' + 'closed TheList closed, and emits none of the above', cb => { w = make( { queryOptions: qoFT }, { 'item-active-change': o => o } ); _setInput('Q'); clock.tick(300); _listEx().should.equal(false); // TheList stays closed. _setInput('Q'); // Results in no matches, not even a ListItemLiteral. clock.tick(300); vueTick(() => { _listEx ().should.equal(false); // TheList stays closed again. _emitLO ().should.equal(false); // } It emitted no 'list-open', .. _emitLC ().should.equal(false); // } ..no 'list-close', .. _emitIAC().should.equal(0); // } ..and no 'item-active-change'. cb(); }); }); it('trims the input string (i.e. removes front/end whitespace) ' + 'before search', () => { w = make({}); _setInput(' \t ab '); // Matches `e2`. clock.tick(300); _listEx () .should.equal(true); _itemPST(0).should.equal('ab'); }); it('emits both \'input-change\' and \'input\' + search-string, ' + 'for absent `initialValue`', () => { w = make({}); clock.tick(300); _emitIC(0).should.equal(''); _emitI (0).should.equal(''); }); it('emits \'input-change\' + trimmed search-string, and \'input\' + ' + 'full search-string, for a given `initialValue`', () => { w = make({ initialValue: ' \t a ' }); clock.tick(300); _emitIC(0).should.equal('a'); _emitI (0).should.equal(' \t a '); }); it('emits \'input-change\' and \'input\', when changing TheInput', () => { w = make({}); clock.tick(50); _setInput(' \t ab '); clock.tick(300); _emitIC(1).should.equal('ab'); _emitI (1).should.equal(' \t ab '); _setInput('x '); clock.tick(300); _emitIC(2).should.equal('x'); _emitI (2).should.equal('x '); }); it('emits no \'input-change\', nor changes TheList, if adding only ' + 'whitespace to input string\'s start/end; but emits \'input\'', () => { w = make({ initialValue: 'a' }, {}); // Matches `e1` and `e2`. _focus(); clock.tick(300); _emitIC (0).should.equal('a'); _emitI (0).should.equal('a'); _listLen() .should.equal(2); _itemPST(0).should.equal('a'); _itemPST(1).should.equal('ab'); _setInput(' a\t '); clock.tick(300); _emitIC (1).should.equal(0); // Didn't emit a second 'input-change'. _emitI (1).should.equal(' a\t '); // Did emit a second 'input'. _listLen() .should.equal(2); // } TheList did not change. _itemPST(0).should.equal('a'); // } _itemPST(1).should.equal('ab'); // } }); it('makes the first ListItem the active one again, ' + 'after updating TheList', cb => { w = make({ initialValue: 'b' }); // Matches `e3/5/4/2` + item-literal. _focus(); clock.tick(300); vueTick(() => { // Somehow, vue-test-utils needs this to add item-literal. _listLen().should.equal(5); _item (0).classes().should.contain('item-state-active'); _keyDown(); // Make the second item ('bcc') the active one. _item (1).classes().should.contain('item-state-active'); _setInput('bc'); // Change input (this makes TheList stale), .. clock.tick(200); // and wait for query result to arrive. vueTick(() => { _listLen().should.equal(4); // TheList changed: one match less. _item(0).classes().should.contain('item-state-active'); // Item1=activ. cb(); }); }); }); it('makes the first ListItem the active one again, ' + 'after updating TheList; also when reopening a closed TheList', cb => { w = make({ initialValue: 'b' }); _focus(); clock.tick(300); vueTick(() => { // NOTE: somehow, the following line is needed under the current version // of 'vue-test-utils'@1.0.0-beta.24. It 'touches' TheList's existence. _listEx().should.equal(true); _keyDown(); // Makes the second item the active one. _item (1).classes().should.contain('item-state-active'); _keyEsc(); // Closes TheList again. _listEx().should.equal(false); _setInput('bc'); // Launches new query. clock.tick(200); vueTick(() => { _listEx ().should.equal(true); // TheList re-opened. _listLen().should.equal(4); _item(0).classes().should.contain('item-state-active'); cb(); }); }); }); it('makes the first ListItem the active one again, after updating ' + 'TheList; also if there remains only a ListItemLiteral', cb => { w = make({ initialValue: 'b' }); _focus(); clock.tick(300); vueTick(() => { _listEx().should.equal(true); _keyDown(); // Makes the second item the active one. _item(1).classes().should.contain('item-state-active'); _setInput('Q'); // This will give no matches. clock.tick(200); vueTick(() => { _listLen().should.equal(1); // Only the ListItemLiteral. _item(0).classes().should.contain('item-type-literal'); _item(0).classes().should.contain('item-state-active'); cb(); }); }); }); it('changes ListItemLiteral\'s content along with TheInput', cb => { w = make({ initialValue: 'QQQ' }); // This will give no matches. _focus(); clock.tick(300); vueTick(() => { _itemLOnly().should.equal(true); // Only the ListItemLiteral. _itemLT().should.contain('QQQ'); _setInput('RRR'); // This will give no matches. clock.tick(200); vueTick(() => { _itemLOnly().should.equal(true); // Only the ListItemLiteral. _itemLT().should.contain('RRR'); cb(); }); }); }); it('on focus, it emits `focus`; and does not open TheList ' + '(if not given an initialValue)', () => { w = make({}, {}); clock.tick(1000); _listEx().should.equal(false); _focus(); _emitF().should.equal(true); clock.tick(1000); _listEx().should.equal(false); }); it('on focus, it emits `focus`; and opens TheList ' + '(if given an `initialValue`) and emits `list-open`', () => { w = make({ initialValue: 'ab' }, {}); // Part 1: TheList does not open without `_focus()`. clock.tick(1000); _listEx().should.equal(false); // Part 2: TheList opens after 'focus' and after query results arrive. _focus(); _emitF().should.equal(true); clock.tick(200); _listEx().should.equal(true); _emitLO().should.equal(true); }); it('on blur, it emits `blur`; and closes TheList and emits ' + '`list-close`', () => { w = make({ initialValue: 'ab' }); _focus(); clock.tick(300); _listEx().should.equal(true); _blur(); _emitB ().should.equal(true); _listEx().should.equal(false); _emitLC().should.equal(true); }); it('on focus, it does not reopen a hard-closed TheList ' + '(= closed with Esc)', () => { w = make({ initialValue: 'ab' }); _focus(); clock.tick(300); _listEx().should.equal(true); // TheList is open, .. _emitLO().should.equal(true); // ..and it emitted a 'list-open'. _keyEsc(); // Pressing Esc.. _emitB ().should.equal(false); // ..does not emit 'blur', .. _listEx().should.equal(false); // ..but it closes TheList.. _emitLC().should.equal(true); // ..and this emits 'list-close'. _blur(); // Next, after a blur.. _focus(); // ..and a new focus event, _listEx().should.equal(false); // TheList stays closed, .. _emitLO(1).should.equal(false); // ..and emits no 2nd 'list-open'. }); it('on focus[=>open] (resp. blur/Esc[=>close]), it emits ' + '`item-active-change` + the active item\'s ID (resp. false)', () => { w = make({ initialValue: 'ab' }); _focus(); clock.tick(300); _listEx() .should.equal(true); _emitIAC(0).id.should.equal(e2.id); // On open: i-a-c + item `e2`'s ID. _blur(); _listEx() .should.equal(false); _emitIAC(1) .should.equal(false); // On blur-close: i-a-c + `false`. _focus(); _listEx() .should.equal(true); _emitIAC(2).id.should.equal(e2.id); // On reopen: same as first. _keyEsc(); _listEx() .should.equal(false); _emitIAC(3) .should.equal(false); // On Esc-closing: i-a-c + `false`. }); it('on Ctrl+Enter, when TheInput is empty, ' + 'it closes TheList and emits `key-ctrl-enter`', () => { w = make({ initialValue: '', queryOptions: qoFT }, {}); _focus(); clock.tick(300); _listLen().should.equal(1); // TheList contains a fixedTerm-match item. _keyCEnter(); _listEx() .should.equal(false); _emitLC() .should.equal(true); _emitCEnt().should.equal(true); }); it('on Ctrl+Enter, when TheInput contains text without string-code, ' + 'it closes TheList and emits `key-ctrl-enter`', () => { w = make({ initialValue: 'ab' }, {}); _focus(); clock.tick(300); _inputV().should.equal('ab'); _listLen().should.equal(1); _keyCEnter(); clock.tick(1000); _inputV().should.equal('ab'); _listEx() .should.equal(false); _emitLC() .should.equal(true); _emitCEnt().should.equal(true); }); it(['on Ctrl+Enter, when TheInput contains a string-code (e.g. \'\\beta\'),', 'it changes TheInput\'s content (e.g. \'β\'), updates TheList, and emits', 'it with `input/-change`; and `item-active-change` for a ListItemLiteral'] .join('\n '), cb => { w = make({ initialValue: 'ab\\beta/beta' }); _focus(); clock.tick(300); vueTick(() => { _itemLOnly().should.equal(true); // Only ListItemLiteral, in open list. _inputV ().should.equal('ab\\beta/beta'); _emitIC ().should.equal('ab\\beta/beta'); // Emits both 'input-change'.. _emitI ().should.equal('ab\\beta/beta'); // .. and 'input'. _emitIAC().should.equal('ab\\beta/beta'); _keyCEnter(); _inputV() .should.equal('abββ'); // Ctrl+Enter changed TheInput's.. _emitIC(1).should.equal('abββ'); // ..value, and also emits this. _emitI (1).should.equal('abββ'); clock.tick(200); // Wait for the new query result. vueTick(() => { _itemLOnly(); // Still only a ListItemLiteral, in an open TheList. _itemLT () .should.contain('abββ'); // It contains TheInput's value. _emitIAC(1).should.equal('abββ'); // It emitted item-active-change. _emitCEnt().should.equal(false); // It emitted no key-ctrl-enter. cb(); }); }); }); it('on Shift+Enter, it closes TheList and emits `key-shift-enter`', () => { w = make({ initialValue: 'a' }, {}); _focus(); clock.tick(300); _listLen().should.equal(2); _keySEnter(); _listEx() .should.equal(false); _emitLC() .should.equal(true); _emitSEnt().should.equal(true); }); it('on key-Up/Down, changes the active ListItem, and emits ' + '`item-active-change`', cb => { w = make({ initialValue: 'b' }); // Matches `e3/5/4/2` + item-literal. var es = [e3, e5, e4, e2]; // Matching entries in match-order, excl. itemL. _focus(); clock.tick(300); vueTick(() => { _listLen() .should.equal(5); _item (0).classes().should.contain('item-state-active'); _emitIAC(0).id.should.equal(es[0].id); _keyDown(); // Activate item 2. _item (1).classes().should.contain('item-state-active'); _emitIAC(1).id.should.equal(es[1].id); _onlyOneItemIsActive(); _keyDown(); // Activate item 3. _item (2).classes().should.contain('item-state-active'); _emitIAC(2).id.should.equal(es[2].id); _onlyOneItemIsActive(); _keyUp(); // Activate item 2. _item (1).classes().should.contain('item-state-active'); _emitIAC(3).id.should.equal(es[1].id); _onlyOneItemIsActive(); _keyUp(); // Activate item 1. _item (0).classes().should.contain('item-state-active'); _emitIAC(4).id.should.equal(es[0].id); _onlyOneItemIsActive(); cb(); }); }); it('on key-Up/Down, cycles through a TheList without a ListItemLiteral, ' + 'and emits', cb => { w = make( { initialValue: 'b' }, { // We add no item-lit. listener, .. 'item-active-change': o => o // ..but still add one for this. .. } ); // ..This matches `e3/5/4/2`; no ListItemLiteral. var es = [e3, e5, e4, e2]; // Matching entries in match-order. _focus(); clock.tick(300); vueTick(() => { _listLen() .should.equal(4); // No ListItemLiteral. _itemL () .exists().should.equal(false); _item (0).classes().should.contain('item-state-active'); _emitIAC(0).id.should.equal(es[0].id); _keyUp(); // Activate last item: `e2`. _item (3).classes().should.contain('item-state-active'); _emitIAC(1).id.should.equal(es[3].id); _onlyOneItemIsActive(); _keyUp(); // Activate last-but-one item. _item (2).classes().should.contain('item-state-active'); _emitIAC(2).id.should.equal(es[2].id); _onlyOneItemIsActive(); _keyDown(); // Activate last item. _item (3).classes().should.contain('item-state-active'); _emitIAC(3).id.should.equal(es[3].id); _onlyOneItemIsActive(); _keyDown(); // Activate item 1. _item (0).classes().should.contain('item-state-active'); _emitIAC(4).id.should.equal(es[0].id); _onlyOneItemIsActive(); _keyDown(); // Activate item 2. _item (1).classes().should.contain('item-state-active'); _emitIAC(5).id.should.equal(es[1].id); _onlyOneItemIsActive(); cb(); }); }); it('on key-Up/Down, cycles through a TheList with a ListItemLiteral, ' + 'and emits', cb => { w = make({ initialValue: 'b' }); // Matches `e3/5/4/2` + item-literal. var es = [e3, e5, e4, e2]; // Matching entries in match-order, excl. itemL. _focus(); clock.tick(300); vueTick(() => { _listLen() .should.equal(5); _itemL () .exists().should.equal(true); _item (0).classes().should.contain('item-state-active'); _emitIAC(0).id.should.equal(es[0].id); _keyUp(); // Activate last item: the ListItemLiteral. _item (4).classes().should.contain('item-state-active'); _emitIAC(1) .should.equal('b'); // The literal input-string. _onlyOneItemIsActive(); _keyUp(); // Activate last-but-one item: `e2`. _item (3).classes().should.contain('item-state-active'); _emitIAC(2).id.should.equal(es[3].id); _onlyOneItemIsActive(); _keyDown(); // Activate last item. _item (4).classes().should.contain('item-state-active'); _emitIAC(3) .should.equal('b'); _onlyOneItemIsActive(); _keyDown(); // Activate item 1. _item (0).classes().should.contain('item-state-active'); _emitIAC(4).id.should.equal(es[0].id); _onlyOneItemIsActive(); _keyDown(); // Activate item 2. _item (1).classes().should.contain('item-state-active'); _emitIAC(5).id.should.equal(es[1].id); _onlyOneItemIsActive(); cb(); }); }); it('on key-Up/Down, for only one ListItem: does nothing', cb => { w = make({