UNPKG

@schukai/monster

Version:

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

1,292 lines (1,080 loc) 76 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> `; function createJsonResponse(data, status = 200) { let headers = new Map(); headers.set('content-type', 'application/json'); return Promise.resolve({ ok: status >= 200 && status < 300, status, headers, text: function () { return Promise.resolve(JSON.stringify(data)); } }); } function createDeferred() { let resolve; let reject; const promise = new Promise((res, rej) => { resolve = res; reject = rej; }); return {promise, resolve, reject}; } function waitForCondition(check, {timeout = 4000, interval = 25} = {}) { return new Promise((resolve, reject) => { const start = Date.now(); const poll = () => { try { if (check()) { resolve(); return; } } catch (e) { reject(e); return; } if (Date.now() - start >= timeout) { reject(new Error('Timed out while waiting for test condition.')); return; } setTimeout(poll, interval); }; poll(); }); } let Select, SelectStyleSheet, getDefaultSelectPopperPositionProfile, resolveSelectListDimension, resolveSelectPopperWidthConstraints, resolveSelectVisibleRect, resolveSelectViewportMetrics, 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/host/host.mjs").then(() => { return import("../../../../source/components/form/popper-button.mjs"); }).then(() => { return import("../../../../source/components/form/select.mjs"); }).then((m) => { Select = m['Select']; getDefaultSelectPopperPositionProfile = m['getDefaultSelectPopperPositionProfile']; resolveSelectListDimension = m['resolveSelectListDimension']; resolveSelectPopperWidthConstraints = m['resolveSelectPopperWidthConstraints']; resolveSelectVisibleRect = m['resolveSelectVisibleRect']; resolveSelectViewportMetrics = m['resolveSelectViewportMetrics']; return import("../../../../source/components/form/stylesheet/select.mjs"); }).then((m) => { SelectStyleSheet = m['SelectStyleSheet']; 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('Popper sizing', function () { this.timeout(5000); afterEach(() => { let mocks = document.getElementById('mocks'); mocks.innerHTML = ""; }); it('should allow the popper to become wider than a narrow control', function (done) { const mocks = document.getElementById('mocks'); const select = document.createElement('monster-select'); select.setOption('options', [ {label: 'Alpha', value: 'alpha'}, {label: 'Beta', value: 'beta'} ]); mocks.appendChild(select); const shadowRoot = select.shadowRoot; const control = shadowRoot.querySelector('[data-monster-role=control]'); const popper = shadowRoot.querySelector('[data-monster-role=popper]'); control.getBoundingClientRect = () => ({ width: 120, height: 36, top: 100, left: 40, right: 160, bottom: 136, x: 40, y: 100 }); setTimeout(() => { try { shadowRoot.querySelector('[data-monster-role=container]').click(); setTimeout(() => { try { expect(popper.style.minWidth).to.equal('240px'); expect(popper.dataset.monsterWidthBehavior).to.equal('preferred'); expect(popper.dataset.monsterPreferredWidth).to.equal('240'); done(); } catch (e) { done(e); } }, 80); } catch (e) { done(e); } }, 20); }); it('should keep the option list height tight for short lists', function () { const result = resolveSelectListDimension({ visibleOptionHeights: [28, 28], maxVisibleOptions: 20, availableHeight: 500, padding: 0, margin: 0 }); expect(result.desiredHeight).to.equal(56); expect(result.maxHeight).to.equal(500); expect(result.overflowY).to.equal('hidden'); }); it('should keep option list scroll layout stable while overflow toggles', function () { const cssText = Array.from(SelectStyleSheet.cssRules) .map((rule) => rule.cssText) .join('\n') .replace(/\s+/g, ''); expect(cssText).to.contain('scrollbar-gutter:stable'); expect(cssText).to.not.contain('transition:height'); }); it('should refresh the content max height when the available popper height grows again', function (done) { const mocks = document.getElementById('mocks'); const select = document.createElement('monster-select'); select.setOption('options', Array.from({length: 24}, (_, index) => ({ label: `Option ${index + 1}`, value: `${index + 1}` }))); mocks.appendChild(select); const shadowRoot = select.shadowRoot; const control = shadowRoot.querySelector('[data-monster-role=control]'); const popper = shadowRoot.querySelector('[data-monster-role=popper]'); const content = shadowRoot.querySelector('[part=content]'); const options = shadowRoot.querySelector('[data-monster-role=options]'); control.getBoundingClientRect = () => ({ width: 200, height: 36, top: 100, left: 40, right: 240, bottom: 136, x: 40, y: 100 }); setTimeout(() => { try { shadowRoot.querySelector('[data-monster-role=container]').click(); setTimeout(() => { try { content.style.maxHeight = '20px'; options.style.height = '20px'; options.style.maxHeight = '20px'; select.calcAndSetOptionsDimension(); expect(parseFloat(content.style.maxHeight)).to.be.greaterThan(20); expect(content.style.maxHeight).to.equal(options.style.maxHeight); expect(parseFloat(popper.style.maxHeight)).to.be.at.least(parseFloat(content.style.maxHeight)); done(); } catch (e) { done(e); } }, 80); } catch (e) { done(e); } }, 20); }); it('should reserve popper chrome height before sizing the option list', async function () { const mocks = document.getElementById('mocks'); const select = document.createElement('monster-select'); select.setOption('showMaxOptions', 10); select.setOption('filter.position', 'popper'); select.setOption('options', Array.from({length: 10}, (_, index) => ({ label: `Option ${index + 1}`, value: `${index + 1}` }))); mocks.appendChild(select); await waitForCondition(() => { return select.shadowRoot.querySelector('[data-monster-role=container]') instanceof HTMLElement; }); await waitForCondition(() => { return select.shadowRoot.querySelectorAll('[data-monster-role=option]').length === 10; }); const shadowRoot = select.shadowRoot; const control = shadowRoot.querySelector('[data-monster-role=control]'); const popper = shadowRoot.querySelector('[data-monster-role=popper]'); const content = shadowRoot.querySelector('[part=content]'); const options = shadowRoot.querySelector('[data-monster-role=options]'); const filterControl = shadowRoot.querySelector('[part=popper-filter-control]'); const pagination = shadowRoot.querySelector('[data-monster-role=pagination]'); const remoteInfo = shadowRoot.querySelector('[data-monster-role=remote-info]'); control.getBoundingClientRect = () => ({ width: 300, height: 50, top: 100, left: 40, right: 340, bottom: 150, x: 40, y: 100 }); popper.getBoundingClientRect = () => ({ width: 300, height: 500, top: 150, left: 40, right: 340, bottom: 650, x: 40, y: 150 }); filterControl.getBoundingClientRect = () => ({ width: 260, height: 40, top: 0, left: 0, right: 260, bottom: 40, x: 0, y: 0 }); pagination.getBoundingClientRect = () => ({ width: 260, height: 34, top: 0, left: 0, right: 260, bottom: 34, x: 0, y: 0 }); remoteInfo.getBoundingClientRect = () => ({ width: 260, height: 33, top: 0, left: 0, right: 260, bottom: 33, x: 0, y: 0 }); for (const option of shadowRoot.querySelectorAll('[data-monster-role=option]')) { option.getBoundingClientRect = () => ({ width: 260, height: 50, top: 0, left: 0, right: 260, bottom: 50, x: 0, y: 0 }); } select.calcAndSetOptionsDimension(); const optionHeight = parseFloat(options.style.height); expect(optionHeight).to.be.greaterThan(0); expect(optionHeight).to.be.lessThan(500); expect(options.style.maxHeight).to.equal(options.style.height); expect(options.style.overflowY).to.equal('auto'); expect(content.style.maxHeight).to.equal(options.style.height); expect(parseFloat(popper.style.maxHeight)).to.be.greaterThan(optionHeight); }); it('should clamp preferred width to the visible viewport width', function () { const result = resolveSelectPopperWidthConstraints({ controlWidth: 120, availableWidth: 176 }); expect(result.preferredWidth).to.equal(240); expect(result.maxWidth).to.equal(176); }); it('should keep start placement while enabling cross-axis shifting', function () { const result = getDefaultSelectPopperPositionProfile(); expect(result.placement).to.equal('bottom-start'); expect(result.middleware).to.deep.equal([ 'flip', 'offset:4', 'shift:crossAxis', 'size' ]); }); it('should prefer the larger live viewport metrics after a resize', function () { const result = resolveSelectViewportMetrics({ layoutWidth: 1400, layoutHeight: 900, visualWidth: 1024, visualHeight: 700, offsetLeft: 20, offsetTop: 30, padding: 12 }); expect(result.width).to.equal(1400); expect(result.height).to.equal(900); expect(result.left).to.equal(20); expect(result.top).to.equal(30); expect(result.padding).to.equal(12); }); it('should intersect viewport and split container for the visible rect', function () { const result = resolveSelectVisibleRect({ viewportMetrics: { width: 1400, height: 900, left: 0, top: 0, padding: 12 }, boundaryRect: { left: 100, top: 80, right: 620, bottom: 700 } }); expect(result.left).to.equal(100); expect(result.top).to.equal(80); expect(result.right).to.equal(620); expect(result.bottom).to.equal(700); expect(result.width).to.equal(520); expect(result.height).to.equal(620); }); it('should keep the popper closed when a queued resize update settles after an outside click', function (done) { this.timeout(4000); const mocks = document.getElementById('mocks'); const originalResizeObserver = global.ResizeObserver; const resizeObservers = []; global.ResizeObserver = class extends ResizeObserverMock { constructor(callback) { super(callback); resizeObservers.push(this); } }; const restoreResizeObserver = () => { global.ResizeObserver = originalResizeObserver; }; const fail = (error) => { restoreResizeObserver(); done(error); }; const finish = () => { restoreResizeObserver(); done(); }; const select = document.createElement('monster-select'); select.setOption('type', 'checkbox'); select.setOption('features.clear', true); select.setOption('features.clearAll', true); select.setOption('options', [ {label: 'Alpha', value: 'alpha'}, {label: 'Beta', value: 'beta'} ]); mocks.appendChild(select); setTimeout(() => { try { const shadowRoot = select.shadowRoot; const container = shadowRoot.querySelector('[data-monster-role=container]'); const control = shadowRoot.querySelector('[data-monster-role=control]'); const popper = shadowRoot.querySelector('[data-monster-role=popper]'); const statusBadge = shadowRoot.querySelector('[data-monster-role=status-or-remove-badges]'); container.click(); setTimeout(() => { try { const optionControl = shadowRoot.querySelector('[data-monster-role=option-control]'); expect(optionControl).to.exist; expect(resizeObservers.length).to.be.at.least(1); optionControl.checked = true; optionControl.dispatchEvent(new Event('input', { bubbles: true, composed: true })); optionControl.dispatchEvent(new Event('change', { bubbles: true, composed: true })); setTimeout(() => { try { resizeObservers[0].triggerResize([]); document.body.dispatchEvent(new window.MouseEvent('click', { bubbles: true, composed: true })); setTimeout(() => { try { expect(control.className).to.equal(''); expect(statusBadge.className).to.equal('clear'); expect(popper.style.display).to.equal('none'); } catch (e) { return fail(e); } finish(); }, 50); } catch (e) { fail(e); } }, 20); } catch (e) { fail(e); } }, 20); } catch (e) { fail(e); } }, 20); }); }); describe('Host dismissal', function () { afterEach(() => { let mocks = document.getElementById('mocks'); mocks.innerHTML = ""; }); it('should close only the nested select popper on host outside-pointer dismissal', function (done) { this.timeout(4000); const mocks = document.getElementById('mocks'); const host = document.createElement('monster-host'); const button = document.createElement('monster-popper-button'); const select = document.createElement('monster-select'); const outside = document.createElement('div'); const dismissEventType = typeof global.PointerEvent === 'function' ? 'pointerdown' : 'mousedown'; outside.textContent = 'outside'; select.setOption('options', [ {label: 'Alpha', value: 'alpha'}, {label: 'Beta', value: 'beta'} ]); select.setOption('filter.position', 'popper'); select.setOption('features.closeOnSelect', true); button.setOption('content', select); host.appendChild(button); host.appendChild(outside); mocks.appendChild(host); setTimeout(() => { try { button.showDialog(); setTimeout(() => { try { const buttonShadowRoot = button.shadowRoot; const selectShadowRoot = select.shadowRoot; const selectContainer = selectShadowRoot.querySelector( '[data-monster-role=container]', ); expect(buttonShadowRoot).to.exist; expect(selectShadowRoot).to.exist; expect(selectContainer).to.exist; selectContainer.click(); setTimeout(() => { try { outside.dispatchEvent( new window.MouseEvent(dismissEventType, { bubbles: true, composed: true }), ); setTimeout(() => { try { expect( selectShadowRoot.querySelector( '[data-monster-role=control]', ).className, ).to.equal(''); expect( selectShadowRoot.querySelector( '[data-monster-role=popper]', ).style.display, ).to.equal('none'); expect( buttonShadowRoot.querySelector( '[data-monster-role=control]', ).className, ).to.equal('open'); expect( buttonShadowRoot.querySelector( '[data-monster-role=popper]', ).style.display, ).to.equal('block'); } catch (e) { return done(e); } done(); }, 50); } catch (e) { done(e); } }, 50); } catch (e) { done(e); } }, 50); } catch (e) { done(e); } }, 50); }); }); 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 fetchRemotePage = (value) => { return select.fetch(`https://example.com/items?filter=${encodeURIComponent(value)}&page=1`); }; setTimeout(() => { fetchRemotePage('a') .then(() => { const options = select.getOption('options'); const pager = pagination(); 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); return fetchRemotePage('b') .then(() => Promise.reject(new Error('Expected remote fetch to fail'))) .catch((e) => { if (e.message === 'Expected remote fetch to fail') { throw e; } const optionsAfterError = select.getOption('options'); const pager = pagination(); 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(""); }); }) .then(() => done()) .catch((e) => done(e)); }, 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 expose styling parts for container, filter messages and pagination', function (done) { let mocks = document.getElementById('mocks'); const select = document.createElement('monster-select'); select.setOption('filter.mode', 'remote'); mocks.appendChild(select); setTimeout(() => { try { const container = select.shadowRoot.querySelector('[data-monster-role=container]'); const selectionMessages = select.shadowRoot.querySelector('[data-monster-role=selection-messages]'); const pagination = select.shadowRoot.querySelector('[data-monster-role=pagination]'); expect(container.getAttribute('part')).to.equal('container'); expect(selectionMessages.getAttribute('part')).to.equal('selection-messages'); expect(pagination.getAttribute('part')).to.equal('pagination'); } 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'}, {}, {label: '', value: 'empty-label'}, {label: ' ', value: 'blank-label'} ]); 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'); expect(options[3].label).to.equal('empty-label'); expect(options[4].label).to.equal('blank-label'); } catch (e) { return done(e); } done(); }; setTimeout(poll, 50); }); it('should fall back to the option value when imported labels resolve empty', function () { let mocks = document.getElementById('mocks'); const select = document.createElement('monster-select'); select.setOption('mapping.selector', '*'); select.setOption('mapping.labelTemplate', '${name}'); select.setOption('mapping.valueTemplate', '${id}'); mocks.appendChild(select); select.importOptions([ {id: 'company-1', name: ''}, {id: 'company-2', name: ' '}, {id: 'company-3', name: 'Visible label'} ]); const options = select.getOption('options'); expect(options[0].label).to.equal('company-1'); expect(options[1].label).to.equal('company-2'); expect(options[2].label).to.equal('Visible label'); }); 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 { expect(lookupCount).to.equal(0); } catch (e) { mockIntersectionObserver.restore(); return done(e); } mockIntersectionObserver.restore(); done(); }, 150); }); it('should not eagerly fetch remote info when showRemoteInfo is disabled', function (done) { this.timeout(3000); let mocks = document.getElementById('mocks'); const requests = []; const previousFetch = global['fetch']; global['fetch'] = function (url) { requests.push(String(url)); return createJsonResponse({ items: [ {id: 'alpha', name: 'Alpha'} ], pagination: { total: 3, page: 2, 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('remoteInfo.url', 'https://example.com/remote-info'); select.setOption('features.showRemoteInfo', false); mocks.appendChild(select); setTimeout(() => { try { expect(requests).to.deep.equal([]); } catch (e) { global['fetch'] = previousFetch; return done(e); } select.fetch('https://example.com/items?filter=*&page=2') .then(() => { try { const pagination = select.shadowRoot.querySelector('[data-monster-role=pagination]'); expect(requests).to.deep.equal([ 'https://example.com/items?filter=*&page=2' ]); expect(select.getOption('total')).to.equal(3); expect(pagination.getOption('currentPage')).to.equal(2); expect(pagination.getOption('pages')).to.equal(3); expect(pagination.getOption('objectsPerPage')).to.equal(1); } catch (e) { return done(e); } finally { global['fetch'] = previousFetch; } done(); }) .catch((e) => { global['fetch'] = previousFetch; done(e); }); }, 150); }); it('should defer remote info fetching until the dropdown is opened', async function () { this.timeout(4000); let mocks = document.getElementById('mocks'); const requests = []; const remoteInfoUrl = 'https://example.com/remote-info'; global['fetch'] = function (url) { requests.push(String(url)); return createJsonResponse({ pagination: { total: 5 } }); }; const select = document.createElement('monster-select'); select.setOption('filter.mode', 'remote'); select.setOption('filter.position', 'popper'); select.setOption('mapping.total', 'pagination.total'); select.setOption('remoteInfo.url', remoteInfoUrl); select.setOption('options', [ {label: 'Alpha', value: 'alpha'} ]); mocks.appendChild(select); await waitForCondition(() => { return select.shadowRoot.querySelector('[data-monster-role=container]') instanceof HTMLElement; }); expect(requests).to.deep.equal([]); const container = select.shadowRoot.querySelector('[data-monster-role=container]'); container.click(); await waitForCondition(() => requests.includes(remoteInfoUrl)); expect(requests.filter((url) => url === remoteInfoUrl)).to.have.length(1); }); it('should keep total-only remote totals for loaded options', async function () { this.timeout(3000); let mocks = document.getElementById('mocks'); global['fetch'] = function () { return createJsonResponse({ items: [ {id: 'alpha', name: 'Alpha'} ], pagination: { total: 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'); mocks.appendChild(select); await waitForCondition(() => { return select.shadowRoot.querySelector('[data-monster-role=container]') instanceof HTMLElement; }); await select.fetch('https://example.com/items?filter=alpha&page=1'); expect(select.getOption('total')).to.equal(1); expect(select.getOption('messages.total')).to.contain('No additional entries are available'); }); it('should keep the remote-info footer height stable while a remote page request is pending', async function () { this.timeout(4000); let mocks = document.getElementById('mocks'); const firstRequest = createDeferred(); const secondRequest = createDeferred();