vsm-autocomplete
Version:
Vue-component for term+ID lookup based on a vsm-dictionary
1,478 lines (1,167 loc) • 106 kB
JavaScript
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({