UNPKG

@schukai/monster

Version:

Monster is a simple library for creating fast, robust and lightweight websites.

578 lines (470 loc) 21.2 kB
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); }); }); });