@schukai/monster
Version:
Monster is a simple library for creating fast, robust and lightweight websites.
578 lines (470 loc) • 21.2 kB
JavaScript
import {getGlobal} from "../../../../source/types/global.mjs";
import * as chai from 'chai';
import {chaiDom} from "../../../util/chai-dom.mjs";
import {initJSDOM} from "../../../util/jsdom.mjs";
import {setupIntersectionObserverMock} from "../../../util/intersection-mock.mjs";
import {ResizeObserverMock} from "../../../util/resize-observer.mjs";
let expect = chai.expect;
chai.use(chaiDom);
const global = getGlobal();
let html1 = `
<div id="test1">
</div>
`;
let html2 = `
<div id="test2">
<monster-select
tabindex="0"
data-monster-bind="path:values.checkbox"
data-monster-options='{
"url": "mock-data.json",
"mapping": {
"selector": "*",
"valueTemplate": "\${first_name} \${last_name}",
"labelTemplate": "id"
},
"type":"checkbox"
}'></monster-select>
</div>
`;
let Select, fetchReference;
describe('Select', function () {
before(function (done) {
initJSDOM().then(() => {
import("element-internals-polyfill").catch(e => done(e));
fetchReference = global['fetch'];
if (!global.ResizeObserver) {
global.ResizeObserver = ResizeObserverMock;
}
import("../../../../source/components/form/select.mjs").then((m) => {
Select = m['Select'];
done()
}).catch(e => done(e))
});
})
describe('With fetch', function () {
beforeEach((done) => {
let mocks = document.getElementById('mocks');
mocks.innerHTML = html1;
global['fetch'] = function (url, options) {
let headers = new Map;
headers.set('content-type', 'application/json');
return new Promise((resolve, reject) => {
resolve({
ok:true,
status:200,
headers: headers,
text: function () {
return new Promise((resolve2, reject2) => {
let json = JSON.parse(`[
{
"id": 1,
"first_name": "Alexia",
"last_name": "Oughtright",
"email": "aoughtright0@exblog.jp",
"gender": "Agender",
"country": "mn"
},
{
"id": 2,
"first_name": "Beth",
"last_name": "Boddington",
"email": "bboddington1@statcounter.com",
"gender": "Non-binary",
"country": "sy"
},
{
"id": 3,
"first_name": "Shelli",
"last_name": "A'Barrow",
"email": "sabarrow2@google.co.uk",
"gender": "Polygender",
"country": "no"
} ]`);
resolve2(JSON.stringify(json))
})
}
});
})
};
done()
})
afterEach(() => {
let mocks = document.getElementById('mocks');
mocks.innerHTML = "";
global['fetch'] = fetchReference;
})
describe('create from template', function () {
beforeEach(() => {
let mocks = document.getElementById('mocks');
mocks.innerHTML = html2;
});
describe('create from template', function () {
it('should contains monster-select', function () {
expect(document.getElementById('test2')).contain.html('<monster-select');
});
});
});
describe('document.createElement', function () {
it('should instance of select', function () {
const select = document.createElement('monster-select');
expect(select).is.instanceof(Select);
});
it('should have options', function (done) {
this.timeout(5000)
let mocks = document.getElementById('mocks');
const select = document.createElement('monster-select');
select.setOption('url', 'https://monsterjs.org/assets/examples/countries.json') // url is not used in this test, see fetch mock
select.setOption('mapping.selector', '*')
select.setOption('mapping.labelTemplate', '${id}')
select.setOption('mapping.valueTemplate', '${id}')
select.addEventListener('monster-options-set', (e) => {
setTimeout(() => {
try {
const options = select.shadowRoot.querySelectorAll('[data-monster-role=option]');
expect(options.length).is.equal(3);
const optionHtml = select.shadowRoot.querySelector('[data-monster-role=options]');
expect(optionHtml).contain.html('data-monster-insert-reference="options-0"');
expect(optionHtml).contain.html('data-monster-insert-reference="options-1"');
expect(optionHtml).contain.html('data-monster-insert-reference="options-2"');
expect(optionHtml).contain.not.html('data-monster-insert-reference="options-3"');
done();
} catch (e) {
done(e)
}
}, 100)
})
mocks.appendChild(select);
});
});
});
describe('Remote filter pagination', function () {
let requestCount = 0;
beforeEach((done) => {
let mocks = document.getElementById('mocks');
mocks.innerHTML = html1;
requestCount = 0;
global['fetch'] = function () {
requestCount += 1;
let headers = new Map;
headers.set('content-type', 'application/json');
if (requestCount === 1) {
return Promise.resolve({
ok: true,
status: 200,
headers: headers,
text: function () {
return Promise.resolve(JSON.stringify({
items: [
{id: 1, name: "Alpha"}
],
pagination: {
total: 2,
page: 1,
perPage: 1
}
}));
}
});
}
return Promise.resolve({
ok: false,
status: 500,
headers: headers,
text: function () {
return Promise.resolve(JSON.stringify({}));
}
});
};
done();
});
afterEach(() => {
let mocks = document.getElementById('mocks');
mocks.innerHTML = "";
global['fetch'] = fetchReference;
});
it('should reset pagination and clear options on fetch error', function (done) {
this.timeout(5000);
let mocks = document.getElementById('mocks');
const select = document.createElement('monster-select');
select.setOption('url', 'https://example.com/items?filter={filter}&page={page}');
select.setOption('filter.mode', 'remote');
select.setOption('mapping.selector', 'items.*');
select.setOption('mapping.labelTemplate', '${name}');
select.setOption('mapping.valueTemplate', '${id}');
select.setOption('mapping.total', 'pagination.total');
select.setOption('mapping.currentPage', 'pagination.page');
select.setOption('mapping.objectsPerPage', 'pagination.perPage');
mocks.appendChild(select);
const pagination = () => select.shadowRoot.querySelector('[data-monster-role=pagination]');
const triggerFilter = (value) => {
const filterInput = select.shadowRoot.querySelector('[data-monster-role=filter]');
filterInput.value = value;
filterInput.dispatchEvent(new InputEvent('input', {
bubbles: true,
composed: true,
data: value
}));
filterInput.dispatchEvent(new KeyboardEvent('keydown', {
code: 'KeyA',
key: 'a',
bubbles: true,
composed: true
}));
};
const startedAt = Date.now();
const pollLoadedState = () => {
try {
const options = select.getOption('options');
const pager = pagination();
if (
options?.length !== 1 ||
!pager ||
pager.getOption('currentPage') !== 1 ||
pager.getOption('pages') !== 2 ||
pager.getOption('objectsPerPage') !== 1
) {
if (Date.now() - startedAt < 3000) {
return setTimeout(pollLoadedState, 50);
}
}
expect(options.length).to.equal(1);
expect(pager.getOption('currentPage')).to.equal(1);
expect(pager.getOption('pages')).to.equal(2);
expect(pager.getOption('objectsPerPage')).to.equal(1);
triggerFilter('b');
setTimeout(pollErrorState, 50);
} catch (e) {
done(e);
}
};
const pollErrorState = () => {
try {
const optionsAfterError = select.getOption('options');
const pager = pagination();
if (
optionsAfterError?.length !== 0 ||
!pager ||
pager.getOption('currentPage') !== null ||
pager.getOption('pages') !== null ||
pager.getOption('objectsPerPage') !== null ||
select.getOption('total') !== null ||
select.getOption('messages.total') !== ""
) {
if (Date.now() - startedAt < 4500) {
return setTimeout(pollErrorState, 50);
}
}
expect(optionsAfterError.length).to.equal(0);
expect(pager.getOption('currentPage')).to.equal(null);
expect(pager.getOption('pages')).to.equal(null);
expect(pager.getOption('objectsPerPage')).to.equal(null);
expect(select.getOption('total')).to.equal(null);
expect(select.getOption('messages.total')).to.equal("");
} catch (e) {
return done(e);
}
done();
};
setTimeout(() => {
triggerFilter('a');
setTimeout(pollLoadedState, 50);
}, 50);
});
});
describe('document.createElement()', function () {
afterEach(() => {
let mocks = document.getElementById('mocks');
mocks.innerHTML = "";
})
it('should have no options', function (done) {
let mocks = document.getElementById('mocks');
const select = document.createElement('monster-select');
mocks.appendChild(select);
setTimeout(() => {
try {
const options = select.shadowRoot.querySelector('[data-monster-role=options]');
expect(options).is.instanceof(HTMLDivElement);
const a = options.parentNode.outerHTML;
const b = options.childNodes.length;
expect(options.hasChildNodes()).to.be.false;
} catch (e) {
return done(e);
}
done();
}, 0)
});
it('should normalize options without throwing', function (done) {
this.timeout(2000);
let mocks = document.getElementById('mocks');
const select = document.createElement('monster-select');
select.setOption('options', [
{label: 'Alpha'},
{value: 'Beta'},
{}
]);
mocks.appendChild(select);
const startedAt = Date.now();
const poll = () => {
try {
const options = select.getOption('options');
if (options?.[0]?.value !== 'Alpha') {
if (Date.now() - startedAt < 1500) {
return setTimeout(poll, 50);
}
}
expect(options[0].value).to.equal('Alpha');
expect(options[0].label).to.equal('Alpha');
expect(options[0].visibility).to.equal('visible');
expect(options[1].value).to.equal('Beta');
expect(options[1].label).to.equal('Beta');
expect(options[1].visibility).to.equal('visible');
expect(options[2].value).to.be.a('string');
expect(options[2].label).to.equal(options[2].value);
expect(options[2].visibility).to.equal('visible');
} catch (e) {
return done(e);
}
done();
};
setTimeout(poll, 50);
});
it('should not parse options arrays with multiple string entries', function (done) {
this.timeout(2000);
let mocks = document.getElementById('mocks');
const select = document.createElement('monster-select');
select.setOption('options', ['One', 'Two']);
mocks.appendChild(select);
setTimeout(() => {
try {
const options = select.getOption('options');
const error = select.getAttribute('data-monster-error') ?? '';
expect(error).to.not.contain('Unexpected token');
expect(options.length).to.equal(2);
expect(options[0]).to.equal('One');
expect(options[1]).to.equal('Two');
} catch (e) {
return done(e);
}
done();
}, 350);
});
it('should treat null value as empty selection', function (done) {
this.timeout(2000);
let mocks = document.getElementById('mocks');
const select = document.createElement('monster-select');
select.setOption('options', [{label: 'One', value: '1'}]);
mocks.appendChild(select);
setTimeout(() => {
select.value = null;
setTimeout(() => {
try {
const selection = select.getOption('selection');
const error = select.getAttribute('data-monster-error') ?? '';
expect(Array.isArray(selection)).to.equal(true);
expect(selection.length).to.equal(0);
expect(select.value).to.equal('');
expect(error).to.not.contain('unsupported type');
} catch (e) {
return done(e);
}
done();
}, 50);
}, 50);
});
it('should remove the last badge without applyRemoval errors', function (done) {
this.timeout(2000);
let mocks = document.getElementById('mocks');
const select = document.createElement('monster-select');
select.setOption('type', 'checkbox');
select.setOption('features.clear', true);
select.setOption('options', [{label: 'One', value: '1'}]);
mocks.appendChild(select);
select.addEventListener('monster-selection-removed', (event) => {
try {
expect(event.detail.value).to.equal('1');
expect(select.getOption('selection')).to.deep.equal([]);
} catch (e) {
return done(e);
}
done();
}, {once: true});
setTimeout(() => {
try {
select.value = ['1'];
} catch (e) {
return done(e);
}
setTimeout(() => {
try {
const removeBadge = select.shadowRoot.querySelector('[data-monster-role=remove-badge]');
expect(removeBadge).to.be.instanceof(HTMLDivElement);
removeBadge.dispatchEvent(new Event('click', {
bubbles: true,
composed: true
}));
} catch (e) {
return done(e);
}
}, 50);
}, 50);
});
it('should skip lookup for empty equivalent selection values', function (done) {
this.timeout(2000);
let mocks = document.getElementById('mocks');
const mockIntersectionObserver = setupIntersectionObserverMock();
let lookupCount = 0;
global['fetch'] = function () {
lookupCount += 1;
let headers = new Map;
headers.set('content-type', 'application/json');
return Promise.resolve({
ok: true,
status: 200,
headers: headers,
text: function () {
return Promise.resolve(JSON.stringify({
items: [],
pagination: {
total: 0,
page: 1,
perPage: 1
}
}));
}
});
};
const select = document.createElement('monster-select');
select.setOption('url', 'https://example.com/items?filter={filter}&page={page}');
select.setOption('filter.mode', 'remote');
select.setOption('mapping.selector', 'items.*');
select.setOption('mapping.labelTemplate', '${name}');
select.setOption('mapping.valueTemplate', '${id}');
select.setOption('mapping.total', 'pagination.total');
select.setOption('mapping.currentPage', 'pagination.page');
select.setOption('mapping.objectsPerPage', 'pagination.perPage');
select.setOption('empty.equivalents', ['EMPTY']);
select.setOption('selection', [{value: 'EMPTY'}]);
mocks.appendChild(select);
setTimeout(() => {
try {
const observer = mockIntersectionObserver.getInstance();
observer.enterNode();
} catch (e) {
mockIntersectionObserver.restore();
return done(e);
}
setTimeout(() => {
try {
expect(lookupCount).to.equal(0);
} catch (e) {
mockIntersectionObserver.restore();
return done(e);
}
mockIntersectionObserver.restore();
done();
}, 150);
}, 50);
});
});
});