@schukai/monster
Version:
Monster is a simple library for creating fast, robust and lightweight websites.
1,292 lines (1,080 loc) • 76 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>
`;
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();