vsm-autocomplete
Version:
Vue-component for term+ID lookup based on a vsm-dictionary
320 lines (279 loc) • 12.7 kB
JavaScript
import ListItem from './ListItem.vue';
// Some extra, global variables are defined in 'test-setup.js'.
describe('sub/ListItem', () => {
const dictInfo = { id: 'D01', name: 'Dict.1' };
const dictInfoN = { id: '00', name: 'Numbers' };
const item = { dictID: 'D01', id: 'id-1', str: 'abcde', descr: 'Abcde',
type: 'S',
terms: [{ str: 'abcde' }, { str: 'Q' }],
z: { stars: '**' } };
const itemN = { dictID: '00', id: '00:5e+0', str: '5', descr: 'number',
type: 'N' };
const itemN2 = { dictID: '00', id: '00:1.2e+1', str: '12', descr: 'twelve',
type: 'S' }; // A number-str as a queried match, not as VsmDict.-generated.
const itemR = { str: 'it', descr: 'referring term', type: 'R' };
const itemN_shortID = '5e+0';
const itemN2_shortID = '1.2e+1';
const maxStringLengths = {str: Number.MAX_VALUE, strAndDescr: Number.MAX_VALUE};
const maxStringLengthsS = { str: 3, strAndDescr: Number.MAX_VALUE };
const maxStringLengthsD = { str: Number.MAX_VALUE, strAndDescr: 8 };
const maxStringLengthsSD = { str: 3, strAndDescr: 6 };
const vsmDictionary = {
numberMatchConfig: { dictID: '00', conceptIDPrefix: '00:' }
};
// A function for creating a test-component with custom props.
const make = props => shallowMount(ListItem, {
propsData: Object.assign(
// Add at least these 4 props; give them default (but overridable) values:
{ item, maxStringLengths, dictInfo, vsmDictionary },
// A test can override the above and add others, via the `props` argument:
props
)
});
// Shorthands for testing the text/html of a ListItem's different html-parts.
const _str = wrap => wrap.find('.item-part-str' ).text();
const _descr = wrap => wrap.find('.item-part-descr').text();
const _info = wrap => wrap.find('.item-part-info' ).text();
const _extra = wrap => wrap.find('.item-part-extra').html(); // I.e. full HTML.
const _strT = wrap => wrap.find('.item-part-str' ).attributes().title;
const _descrT = wrap => wrap.find('.item-part-descr').attributes().title;
const _infoT = wrap => wrap.find('.item-part-info' ).attributes().title;
it('initializes, when getting only the required props', () => {
shallowMount(ListItem, {
propsData: {
/// searchStr: '',
item,
maxStringLengths,
/// queryOptions: {},
/// customItem: false,
/// dictInfo,
/// vsmDictionary: {}
}
});
});
it('emits a (left-button) click event, with the ListItem\'s `index`', () => {
var wrap = make({ index: 3 });
wrap.trigger('click', { button: 0 }); /// (Or: `wrap.element.click()`).
wrap.emitted('click')[0][0].should.equal(3); // Emits its index: '3'.
wrap.trigger('click', { button: 2 });
wrap.emitted('click').length.should.not.equal(2); // Did not emit 2nd event.
});
it('emits a hover event, with the `index`, ' +
'when it detects a mousemove event', () => {
// Note: not just mouseenter but mouseover/mousemove!
// Like that, after a user had used arrow-up/down, s/he can switch to the
// mouse, without potentially having to leave & enter the intended ListItem,
// if the mouse pointer would already be over it.
var wrap = make({ index: 3 });
wrap.trigger('mousemove');
wrap.emitted('hover')[0][0].should.equal(3);
});
it('on mousedown: ' +
'cancels the event so it doesn\'t steal focus from TheInput; ' +
'and emits a hover event', () => {
var wrap = make({ index: 3 });
var called = 0; // Hack, because `event.isDefaultPrevented()` does not work.
wrap.trigger('mousedown', {
button: 0,
preventDefault: () => { called = 1 }
});
called.should.equal(1); // Cancels the event.
wrap.emitted('hover')[0][0].should.equal(3); // Emits 'hover' event.
});
it('shows `str`, `descr`, `dictID`, and dict-title ' +
'for a normal item; and puts `descr` and `dictID` in parentheses', () => {
var wrap = make({});
_str (wrap).should.equal(item.str);
_descr(wrap).should.equal('(' + item.descr + ')');
_info (wrap).should.equal('(' + item.dictID + ')');
_infoT(wrap).should.equal(item.id + ' in ' + dictInfo.name);
});
it('shows `str`, `descr`, short-form concept-ID, and dict-title ' +
'for number-type items; and puts `descr` in square brackets', () => {
var wrap = make({ item: itemN, dictInfo: dictInfoN });
_str (wrap).should.equal(itemN.str);
_descr(wrap).should.equal('[' + itemN.descr + ']');
_info (wrap).should.equal('(' + itemN_shortID + ')');
_infoT(wrap).should.equal(itemN.id + ' in ' + dictInfoN.name);
wrap = make({ item: itemN2, dictInfo: dictInfoN });
_str (wrap).should.equal(itemN2.str);
_descr(wrap).should.equal('[' + itemN2.descr + ']');
_info (wrap).should.equal('(' + itemN2_shortID + ')');
_infoT(wrap).should.equal(itemN2.id + ' in ' + dictInfoN.name);
});
it('shows `str` and `descr` for a refTerm-type item, no info-part; ' +
'and puts `descr` in square brackets', () => {
var wrap = make({ item: itemR });
_str (wrap).should.equal(itemR.str);
_descr(wrap).should.equal('[' + itemR.descr + ']');
wrap.find('.item-part-info' ).exists().should.equal(false);
});
it('shows only the tail of URI-type IDs', () => {
var wrap = make({ item: {
dictID: 'http://test.org/dictionaries/D02',
id: 'http://test.org/dictionaries/D02/id-2',
str: 'a', descr: 'd', type: 'S', terms: [{ str: 'a' }]
}});
_info (wrap).should.equal('(D02)');
_infoT(wrap).should.equal('id-2 in ' + dictInfo.name);
// Part 2: test for Number-strings.
wrap = make({
dictInfo: Object.assign({}, dictInfoN, {id: 'http://a/D1'}),
item: Object.assign({}, itemN, {dictID: 'http://a/D1', id: 'http://x/ID1'})
});
_info (wrap).should.equal('(ID1)');
_infoT(wrap).should.equal('ID1 in ' + dictInfoN.name);
});
it('shows dictInfo.abbrev or dictInfo.id-URI-tail / etc in the info-part, ' +
'depending on what is available', () => {
var wrap = make({
dictInfo: { id: 'http://a/D01', abbrev: 'AA1', name: 'Dict.1' }
});
_info (wrap).should.equal('(AA1)');
_infoT(wrap).should.equal('id-1 in Dict.1');
wrap = make({ dictInfo: { id: 'http://a/D01', name: 'Dict.1' } });
_info (wrap).should.equal('(D01)');
_infoT(wrap).should.equal('id-1 in Dict.1');
wrap = make({ dictInfo: { id: 'http://a/D01', abbrev: 'AA1' } });
_info (wrap).should.equal('(AA1)');
_infoT(wrap).should.equal('id-1');
wrap = make({ dictInfo: { id: 'http://a/D01' } });
_info (wrap).should.equal('(D01)');
_infoT(wrap).should.equal('id-1');
});
it('omits `dictInfo`-related output if none is given', () => {
var wrap = make({ dictInfo: undefined });
_info (wrap).should.equal('(' + item.dictID + ')');
_infoT(wrap).should.equal(item.id); // I.e. no "+ ' in ' + dictInfo.name".
});
it('applies a custom render function, prop customItem()`, ' +
'which gets all needed data passed as its arguments', () => {
// (This is just a superficial test).
var customItem = function (data) {
var { item, strs, searchStr, maxStringLengths, dictInfo, vsmDictionary }
= data;
return {
str: `--${strs.str}--` + (item.z ? item.z.stars : ''),
strTitle: 'strTitle__',
descr: (item.terms || []).map(t => t.str).join(',') + ',' + strs.descr,
descrTitle: 'descrTitle__',
info: 'info__',
infoTitle: '__' + // Helps testing that all args are passed correctly:
[ item.id, searchStr,
maxStringLengths.str, maxStringLengths.strAndDescr,
dictInfo.id, dictInfo.name, vsmDictionary.numberMatchConfig.dictID,
vsmDictionary.numberMatchConfig.conceptIDPrefix,
strs.str, strs.strTitle, strs.descr, strs.descrTitle,
strs.info, strs.infoTitle, strs.extra ].join('__') + '__',
extra: '<i a="b">extra__</i>'
};
};
var searchStr = 'SRCH';
var w = make({ searchStr, maxStringLengths, dictInfo, customItem });
_str (w).should.equal('--' + item.str + '--**'); // New `strs.str`.
_strT (w).should.equal('strTitle__'); // New `strs.strTitle`.
_descr (w).should.equal(`(abcde,Q,${item.descr})`); // New `strs.descr`.
_descrT(w).should.equal('descrTitle__'); // New `strs.descrTitle`.
_info (w).should.equal('(' + 'info__' + ')'); // New `strs.info`.
_extra (w).should.contain('<i a="b">extra__</i>'); // New `strs.extra`.
// Test that all `customItem()`-data-properties are passed correctly.
var strs = { // This object is built in ListItem's computed `strs()`.
str: item.str,
strTitle: '',
descr: item.descr,
descrTitle: '',
info: item.dictID,
infoTitle: item.id + ' in ' + dictInfo.name,
extra: '' // -> is always given as '', and customItem() may replace it.
};
_infoT(w).should.equal('__' +
[ item.id, searchStr, maxStringLengths.str, maxStringLengths.strAndDescr,
dictInfo.id, dictInfo.name, vsmDictionary.numberMatchConfig.dictID,
vsmDictionary.numberMatchConfig.conceptIDPrefix,
strs.str, strs.strTitle, strs.descr, strs.descrTitle,
strs.info, strs.infoTitle, strs.extra ].join('__') + '__'
);
});
it('accepts HTML-code from the custom render function, ' +
'but secures against `<script>`, `<iframe>`, etc. tags, ' +
'and `"` in title-attributes ', () => {
// (In case some third-party customization functionality is given).
var html = make({
maxStringLengths: maxStringLengthsSD,
dictInfo,
customItem: data => Object.assign(data.strs, {
strTitle: '<x1>"<script>',
str: '<x2>"<script>',
descrTitle: '<x3>"<script>',
descr: '<x4>"<script>',
infoTitle: '<x5>"<script>',
info: '<x6>"<script>',
extra: '<x7>"<script>< script="a"><iframe>< style><textarea\n>'
})
}).html();
///H(html);
html.should.contain('<x1>"<script');
html.should.contain( '<x2>"<script');
html.should.contain('<x3>"<script');
html.should.contain( '<x4>"<script');
html.should.contain('<x5>"<script');
html.should.contain( '<x6>"<script');
html.should.contain( '<x7>"<script');
html.should.contain( '< script=');
html.should.contain( '<iframe');
html.should.contain( '< style');
html.should.contain( '<textarea');
});
it('..and drops empty \'title\' attributes', () => {
var wrap = make({
dictInfo,
customItem: data => Object.assign(data.strs, {
strTitle: '',
str: '',
descrTitle: '',
infoTitle: ''
})
});
expect(_strT (wrap)).to.equal(undefined);
expect(_descrT(wrap)).to.equal(undefined);
expect(_infoT (wrap)).to.equal(undefined);
});
it('..and drops empty \'<span>\' tags, except for \'.item-part-str\'', () => {
var wrap = make({
dictInfo,
customItem: data => Object.assign(data.strs, {
str: '',
descr: '',
info: '',
extra: '',
strTitle: 'test',
descrTitle: 'test',
infoTitle: 'test'
})
});
wrap.find('.item-part-str' ).exists().should.equal(true );
wrap.find('.item-part-descr').exists().should.equal(false);
wrap.find('.item-part-info' ).exists().should.equal(false);
wrap.find('.item-part-extra').exists().should.equal(false);
});
it('limits `str` length, and shows it fully in a \'title\' attribute', () => {
var wrap = make({ maxStringLengths: maxStringLengthsS });
_str (wrap).should.equal(item.str.substr(0, 2) + '…');
_strT (wrap).should.equal(item.str);
_descr(wrap).should.equal('(' + item.descr + ')'); // `descr` wasn't changed.
});
it('limits `descr` length, and shows it fully in a \'title\' attribute', () => {
var wrap = make({ maxStringLengths: maxStringLengthsD });
_str (wrap).should.equal(item.str);
_descr (wrap).should.equal('(' + item.descr.substr(0, 2) + '…' + ')');
_descrT(wrap).should.equal(item.descr);
});
it('limits `str` + `descr` lengths together, ' +
'and shows them fully in \'title\' attributes', () => {
var wrap = make({ maxStringLengths: maxStringLengthsSD });
_str (wrap).should.equal(item.str.substr(0, 2) + '…');
_strT (wrap).should.equal(item.str);
_descr (wrap).should.equal('(' + item.descr.substr(0, 2) + '…' + ')');
_descrT(wrap).should.equal(item.descr);
});
});