@schukai/monster
Version:
Monster is a simple library for creating fast, robust and lightweight websites.
414 lines (351 loc) • 12.4 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 {ResizeObserverMock} from "../../../util/resize-observer.mjs";
let expect = chai.expect;
chai.use(chaiDom);
const global = getGlobal();
// language=html
let html1 = `
<monster-tabs id="mytabs">
<div data-monster-button-label="TAB1">
<div>
Das ist tab 1
</div>
</div>
<div data-monster-button-label="TAB2">
Das ist tab 2
</div>
<div>
Das ist tab 3 ohne button
</div>
<div></div>
</monster-tabs>
`;
// language=html
let htmlActiveSecond = `
<monster-tabs id="mytabs">
<div data-monster-button-label="TAB1">
Das ist tab 1
</div>
<div data-monster-button-label="TAB2" class="active">
Das ist tab 2
</div>
<div data-monster-button-label="TAB3">
Das ist tab 3
</div>
</monster-tabs>
`;
// language=html
let htmlOverflow = `
<monster-tabs id="overflow-tabs">
<div data-monster-button-label="Profil">Profil</div>
<div data-monster-button-label="Adressen">Adressen</div>
<div data-monster-button-label="Kommunikation">Kommunikation</div>
<div data-monster-button-label="Beziehungen">Beziehungen</div>
<div data-monster-button-label="Anstellungen">Anstellungen</div>
<div data-monster-button-label="Einwilligungen">Einwilligungen</div>
<div data-monster-button-label="Einwilligungsstatus">Einwilligungsstatus</div>
<div data-monster-button-label="Berechtigungen">Berechtigungen</div>
<div data-monster-button-label="Dateien">Dateien</div>
<div data-monster-button-label="Kommentare">Kommentare</div>
<div data-monster-button-label="Wiedervorlagen">Wiedervorlagen</div>
</monster-tabs>
`;
let Tabs;
let restoreBoundingClientRect = null;
let restoreResizeObserver = null;
function waitForCondition(check, {timeout = 4000, interval = 10} = {}) {
const startedAt = Date.now();
return new Promise((resolve, reject) => {
const tick = () => {
try {
if (check() === true) {
resolve();
return;
}
} catch (e) {
reject(e);
return;
}
if (Date.now() - startedAt > timeout) {
reject(new Error('condition timed out'));
return;
}
setTimeout(tick, interval);
};
tick();
});
}
describe('Tabs', function () {
before(function (done) {
initJSDOM().then(() => {
import("element-internals-polyfill").catch(e => done(e));
let promises = []
if (!global['crypto']) {
promises.push(import("@peculiar/webcrypto").then((m) => {
const Crypto = m['Crypto'];
global['crypto'] = new Crypto();
}));
}
promises.push(import("../../../../source/components/layout/tabs.mjs").then((m) => {
Tabs = m['Tabs'];
}))
Promise.all(promises).then(()=>{
done();
}).catch(e => done(e))
});
})
describe('document.createElement()', function () {
afterEach(() => {
if (restoreBoundingClientRect instanceof Function) {
restoreBoundingClientRect();
restoreBoundingClientRect = null;
}
if (restoreResizeObserver instanceof Function) {
restoreResizeObserver();
restoreResizeObserver = null;
}
let mocks = document.getElementById('mocks');
mocks.innerHTML = "";
})
it('should have buttons and tabs', function (done) {
let mocks = document.getElementById('mocks');
mocks.innerHTML = html1;
setTimeout(() => {
try {
const tabs = document.getElementById('mytabs')
expect(tabs).is.instanceof(Tabs);
setTimeout(() => {
let nav = tabs.shadowRoot.querySelector('nav');
const buttons = tabs.shadowRoot.querySelectorAll('button[part=button]');
expect(buttons[0]).is.instanceof(HTMLButtonElement);
expect(nav.hasChildNodes()).to.be.true;
expect(buttons.length).to.be.equal(4);
done();
}, 100)
} catch (e) {
return done(e);
}
}, 0)
});
it('should shorten label from content when no explicit label is provided', function (done) {
let mocks = document.getElementById('mocks');
mocks.innerHTML = html1;
setTimeout(() => {
try {
const tabs = document.getElementById('mytabs');
expect(tabs).is.instanceof(Tabs);
setTimeout(() => {
const thirdTab = tabs.children[2];
expect(thirdTab).to.not.equal(undefined);
const reference = thirdTab.getAttribute('id');
expect(reference).to.not.equal(null);
const button = tabs.shadowRoot.querySelector(
`button[part=button][data-monster-tab-reference="${reference}"]`,
);
expect(button).to.not.equal(null);
const labelSpan = button.querySelector('span[data-monster-replace]');
expect(labelSpan).to.not.equal(null);
expect(labelSpan.textContent.trim()).to.equal('Das ist tab…');
done();
}, 100);
} catch (e) {
return done(e);
}
}, 0);
});
it('should open the first tab by default when no tab is predefined as active', function (done) {
let mocks = document.getElementById('mocks');
mocks.innerHTML = html1;
setTimeout(() => {
try {
const tabs = document.getElementById('mytabs');
expect(tabs).is.instanceof(Tabs);
setTimeout(() => {
expect(tabs.children[0].classList.contains('active')).to.equal(true);
expect(tabs.children[1].classList.contains('active')).to.equal(false);
expect(tabs.getActiveTab()).to.equal(tabs.children[0].getAttribute('id'));
done();
}, 100);
} catch (e) {
return done(e);
}
}, 0);
});
it('should keep a predefined active tab instead of opening the first tab', function (done) {
let mocks = document.getElementById('mocks');
mocks.innerHTML = htmlActiveSecond;
setTimeout(() => {
try {
const tabs = document.getElementById('mytabs');
expect(tabs).is.instanceof(Tabs);
setTimeout(() => {
expect(tabs.children[0].classList.contains('active')).to.equal(false);
expect(tabs.children[1].classList.contains('active')).to.equal(true);
expect(tabs.getActiveTab()).to.equal(tabs.children[1].getAttribute('id'));
done();
}, 100);
} catch (e) {
return done(e);
}
}, 0);
});
it('should configure flip and shift middleware for the overflow popper', function (done) {
let mocks = document.getElementById('mocks');
mocks.innerHTML = html1;
setTimeout(() => {
try {
const tabs = document.getElementById('mytabs');
expect(tabs).is.instanceof(Tabs);
const modifiers = tabs.getOption('popper.modifiers');
expect(modifiers.some((entry) => entry?.name === 'flip')).to.equal(true);
expect(modifiers.some((entry) => entry?.name === 'shift')).to.equal(true);
done();
} catch (e) {
return done(e);
}
}, 0);
});
it('should move overflowing tabs into the popper without losing them', function (done) {
this.timeout(5000);
let mocks = document.getElementById('mocks');
const OriginalResizeObserver = window.ResizeObserver;
const originalGlobalResizeObserver = global.ResizeObserver;
class TrackingResizeObserver extends ResizeObserverMock {
static instances = [];
static callbackCount = 0;
constructor(callback) {
super((entries, observer) => {
TrackingResizeObserver.callbackCount++;
callback(entries, observer);
});
TrackingResizeObserver.instances.push(this);
}
}
window.ResizeObserver = TrackingResizeObserver;
global.ResizeObserver = TrackingResizeObserver;
restoreResizeObserver = () => {
window.ResizeObserver = OriginalResizeObserver;
global.ResizeObserver = originalGlobalResizeObserver;
};
const originalGetBoundingClientRect =
global.HTMLElement.prototype.getBoundingClientRect;
restoreBoundingClientRect = () => {
global.HTMLElement.prototype.getBoundingClientRect =
originalGetBoundingClientRect;
};
global.HTMLElement.prototype.getBoundingClientRect = function () {
const role = this.getAttribute?.('data-monster-role');
const label = this.textContent?.trim() || '';
if (role === 'nav') {
return {
width: 480,
height: 40,
top: 0,
left: 0,
right: 480,
bottom: 40,
x: 0,
y: 0,
};
}
if (role === 'button') {
return {
width: Math.max(70, label.length * 11),
height: 40,
top: 0,
left: 0,
right: 0,
bottom: 40,
x: 0,
y: 0,
};
}
return originalGetBoundingClientRect.call(this);
};
mocks.innerHTML = htmlOverflow;
waitForCondition(() => {
const tabs = document.getElementById('overflow-tabs');
const switchButton = tabs?.shadowRoot?.querySelector(
'[data-monster-role="switch"]',
);
const standardButtons = tabs?.getOption?.('buttons.standard') || [];
const popperButtons = tabs?.getOption?.('buttons.popper') || [];
return (
tabs instanceof Tabs &&
switchButton !== null &&
standardButtons.length > 0 &&
popperButtons.length > 0 &&
standardButtons.length + popperButtons.length === 11 &&
switchButton.classList.contains('hidden') === false
);
}).then(() => {
try {
const tabs = document.getElementById('overflow-tabs');
expect(tabs).is.instanceof(Tabs);
const switchButton = tabs.shadowRoot.querySelector(
'[data-monster-role="switch"]',
);
let standardButtons = tabs.getOption('buttons.standard');
let popperButtons = tabs.getOption('buttons.popper');
expect(switchButton).to.not.equal(null);
expect(standardButtons.length).to.be.greaterThan(0);
expect(popperButtons.length).to.be.greaterThan(0);
expect(standardButtons.length + popperButtons.length).to.equal(11);
expect(switchButton.classList.contains('hidden')).to.equal(false);
const resizeObserver = TrackingResizeObserver.instances.find(
(observer) => observer.elements.includes(tabs.shadowRoot.querySelector('[data-monster-role="nav"]')),
);
expect(resizeObserver).to.not.equal(undefined);
resizeObserver.triggerResize([]);
return waitForCondition(() => {
return (
TrackingResizeObserver.callbackCount > 0 &&
tabs.getOption('buttons.standard').length === 11 &&
tabs.getOption('buttons.popper').length === 0
);
}).then(() => {
return waitForCondition(() => {
standardButtons = tabs.getOption('buttons.standard');
popperButtons = tabs.getOption('buttons.popper');
return (
standardButtons.length > 0 &&
popperButtons.length > 0 &&
standardButtons.length + popperButtons.length === 11 &&
new Set(
standardButtons
.concat(popperButtons)
.map((button) => button.reference),
).size === 11
);
});
}).then(() => {
restoreBoundingClientRect();
restoreBoundingClientRect = null;
restoreResizeObserver();
restoreResizeObserver = null;
done();
});
} catch (e) {
restoreBoundingClientRect();
restoreBoundingClientRect = null;
restoreResizeObserver();
restoreResizeObserver = null;
return done(e);
}
}).catch((e) => {
if (restoreBoundingClientRect instanceof Function) {
restoreBoundingClientRect();
restoreBoundingClientRect = null;
}
if (restoreResizeObserver instanceof Function) {
restoreResizeObserver();
restoreResizeObserver = null;
}
done(e);
});
});
});
});