UNPKG

@schukai/monster

Version:

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

546 lines (477 loc) 14.4 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 { 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-message-state-button data-monster-option-labels-button="Save"> Save </monster-message-state-button> </div> `; let MessageStateButton; describe("MessageStateButton", function () { before(function (done) { initJSDOM().then(() => { import("element-internals-polyfill").catch((e) => done(e)); if (!global.ResizeObserver) { global.ResizeObserver = ResizeObserverMock; } import("../../../../source/components/form/message-state-button.mjs") .then((m) => { MessageStateButton = m["MessageStateButton"]; done(); }) .catch((e) => done(e)); }); }); describe("new MessageStateButton", function () { beforeEach(() => { let mocks = document.getElementById("mocks"); mocks.innerHTML = html1; }); afterEach(() => { let mocks = document.getElementById("mocks"); mocks.innerHTML = ""; }); describe("create from template", function () { beforeEach(() => { let mocks = document.getElementById("mocks"); mocks.innerHTML = html2; }); afterEach(() => { let mocks = document.getElementById("mocks"); mocks.innerHTML = ""; }); it("should contain monster-message-state-button", function () { expect(document.getElementById("test2")).contain.html( "<monster-message-state-button", ); }); }); describe("document.createElement", function () { it("should instance of message-state-button", function () { expect( document.createElement("monster-message-state-button"), ).is.instanceof(MessageStateButton); }); }); }); describe("disabled toggle", function () { afterEach(() => { let mocks = document.getElementById("mocks"); mocks.innerHTML = ""; }); it("should sync disabled attribute to inner button", function (done) { let mocks = document.getElementById("mocks"); const button = document.createElement("monster-message-state-button"); button.innerHTML = "Save"; mocks.appendChild(button); setTimeout(() => { try { const inner = button.shadowRoot.querySelector("monster-state-button"); expect(inner).to.exist; button.setAttribute("disabled", ""); setTimeout(() => { try { expect(inner.hasAttribute("disabled")).to.be.true; button.removeAttribute("disabled"); setTimeout(() => { try { expect(inner.hasAttribute("disabled")).to.be.false; button.setAttribute("disabled", ""); setTimeout(() => { try { expect(inner.hasAttribute("disabled")).to.be.true; done(); } catch (e) { done(e); } }, 0); } catch (e) { done(e); } }, 0); } catch (e) { done(e); } }, 0); } catch (e) { done(e); } }, 0); }); }); describe("lifecycle safety", function () { afterEach(() => { let mocks = document.getElementById("mocks"); mocks.innerHTML = ""; }); it("should not throw when auto-hide fires after disconnect", function (done) { let mocks = document.getElementById("mocks"); const button = document.createElement("monster-message-state-button"); button.innerHTML = "Save"; mocks.appendChild(button); const failures = []; const onError = (event) => { failures.push(event?.error || event); }; window.addEventListener("error", onError); setTimeout(() => { try { button.setMessage("Saved").showMessage(10); mocks.removeChild(button); setTimeout(() => { window.removeEventListener("error", onError); try { expect(failures).to.have.length(0); done(); } catch (e) { done(e); } }, 30); } catch (e) { window.removeEventListener("error", onError); done(e); } }, 0); }); }); describe("message content rendering", function () { afterEach(() => { let mocks = document.getElementById("mocks"); mocks.innerHTML = ""; }); it("should mount HTMLElement message content without reparsing rich content", function (done) { let mocks = document.getElementById("mocks"); const button = document.createElement("monster-message-state-button"); button.innerHTML = "Save"; mocks.appendChild(button); const wrapper = document.createElement("div"); const strong = document.createElement("strong"); const inlineControl = document.createElement("button"); strong.textContent = "Saved"; inlineControl.textContent = "Undo"; wrapper.appendChild(strong); wrapper.appendChild(inlineControl); setTimeout(() => { try { button.setMessage(wrapper).showMessage(); setTimeout(() => { try { const message = button.shadowRoot.querySelector( '[data-monster-role="message"]', ); expect(message.firstElementChild).to.equal(wrapper); expect(wrapper.firstElementChild).to.equal(strong); expect(wrapper.lastElementChild).to.equal(inlineControl); expect(inlineControl.isConnected).to.equal(true); done(); } catch (e) { done(e); } }, 0); } catch (e) { done(e); } }, 0); }); }); describe("popper content presentation", function () { afterEach(() => { let mocks = document.getElementById("mocks"); mocks.innerHTML = ""; }); it("should resolve plain prose content to default popper clipping", function (done) { let mocks = document.getElementById("mocks"); const button = document.createElement("monster-message-state-button"); button.innerHTML = "Save"; mocks.appendChild(button); setTimeout(() => { try { button.setMessage( "<div><strong>Saved</strong><p>plain html</p></div>", ); button.showMessage(); setTimeout(() => { try { const content = button.shadowRoot.querySelector('[part="content"]'); const message = button.shadowRoot.querySelector( '[data-monster-role="message"]', ); expect(content).to.exist; expect( content.getAttribute("data-monster-overflow-mode"), ).to.equal("both"); expect( content.getAttribute("data-monster-message-layout"), ).to.equal("prose"); expect( message.getAttribute("data-monster-message-layout"), ).to.equal("prose"); done(); } catch (e) { done(e); } }, 0); } catch (e) { done(e); } }, 0); }); it("should recalculate the open popper after checkbox-driven form growth", function (done) { let mocks = document.getElementById("mocks"); const button = document.createElement("monster-message-state-button"); button.innerHTML = "Save"; mocks.appendChild(button); const form = document.createElement("form"); const label = document.createElement("label"); const checkbox = document.createElement("input"); checkbox.type = "checkbox"; label.appendChild(checkbox); label.append(" show more"); form.appendChild(label); setTimeout(() => { try { button.setMessage(form); button.showMessage(); setTimeout(() => { try { const popper = button.shadowRoot.querySelector( '[data-monster-role="popper"]', ); expect(popper).to.exist; const originalHook = popper.monsterBeforeFloatingUpdate; let updateCount = 0; popper.monsterBeforeFloatingUpdate = () => { updateCount += 1; if (typeof originalHook === "function") { originalHook(); } }; checkbox.checked = true; button.recalcMessage(); setTimeout(() => { try { expect(updateCount).to.be.at.least(1); done(); } catch (e) { done(e); } }, 20); } catch (e) { done(e); } }, 0); } catch (e) { done(e); } }, 0); }); it("should resolve nested select message content to horizontal clipping only", async function () { let mocks = document.getElementById("mocks"); const button = document.createElement("monster-message-state-button"); button.innerHTML = "Save"; mocks.appendChild(button); const wrapper = document.createElement("div"); wrapper.appendChild(document.createElement("monster-select")); button.setMessage(wrapper); button.showMessage(); await waitForCondition(() => { const content = button.shadowRoot?.querySelector('[part="content"]'); return ( content?.getAttribute("data-monster-overflow-mode") === "horizontal" ); }); const content = button.shadowRoot.querySelector('[part="content"]'); const message = button.shadowRoot.querySelector( '[data-monster-role="message"]', ); expect(content).to.exist; expect(content.getAttribute("data-monster-overflow-mode")).to.equal( "horizontal", ); expect(content.getAttribute("data-monster-message-layout")).to.equal( "overlay", ); expect(message.getAttribute("data-monster-message-layout")).to.equal( "overlay", ); }); it("should resolve wide plain content to the wide layout", function (done) { let mocks = document.getElementById("mocks"); const button = document.createElement("monster-message-state-button"); button.innerHTML = "Save"; mocks.appendChild(button); setTimeout(() => { try { const wrapper = document.createElement("div"); const line = document.createElement("div"); line.setAttribute("style", "white-space: nowrap; overflow-x: auto;"); line.textContent = "this is intentionally a single long line to trigger wide layout"; wrapper.appendChild(line); button.setMessage(wrapper); button.showMessage(); setTimeout(() => { try { const content = button.shadowRoot.querySelector('[part="content"]'); const message = button.shadowRoot.querySelector( '[data-monster-role="message"]', ); expect(content).to.exist; expect( content.getAttribute("data-monster-overflow-mode"), ).to.equal("both"); expect( content.getAttribute("data-monster-message-layout"), ).to.equal("wide"); expect( message.getAttribute("data-monster-message-layout"), ).to.equal("wide"); done(); } catch (e) { done(e); } }, 0); } catch (e) { done(e); } }, 0); }); }); describe("message width behavior", function () { let originalInnerWidth; let originalGetBoundingClientRect; beforeEach(() => { originalInnerWidth = window.innerWidth; originalGetBoundingClientRect = HTMLElement.prototype.getBoundingClientRect; }); afterEach(() => { Object.defineProperty(window, "innerWidth", { configurable: true, writable: true, value: originalInnerWidth, }); HTMLElement.prototype.getBoundingClientRect = originalGetBoundingClientRect; let mocks = document.getElementById("mocks"); mocks.innerHTML = ""; }); it("should keep prose content on a readable max width", function (done) { let mocks = document.getElementById("mocks"); const button = document.createElement("monster-message-state-button"); button.innerHTML = "Save"; mocks.appendChild(button); Object.defineProperty(window, "innerWidth", { configurable: true, writable: true, value: 800, }); HTMLElement.prototype.getBoundingClientRect = function () { if (this?.getAttribute?.("data-measurement") === "true") { return { width: 900, height: 120, top: 0, left: 0, right: 900, bottom: 120, }; } return originalGetBoundingClientRect.call(this); }; setTimeout(() => { try { button.setMessage( "<div><p>This long prose content should wrap instead of forcing the message popper to use the full viewport width.</p></div>", ); button.showMessage(); const popper = button.shadowRoot.querySelector( '[data-monster-role="popper"]', ); expect(popper.style.width).to.equal("512px"); expect(popper.style.maxWidth).to.equal("512px"); done(); } catch (e) { done(e); } }, 0); }); it("should allow wide content to grow until the viewport limit", function (done) { let mocks = document.getElementById("mocks"); const button = document.createElement("monster-message-state-button"); button.innerHTML = "Save"; mocks.appendChild(button); Object.defineProperty(window, "innerWidth", { configurable: true, writable: true, value: 800, }); HTMLElement.prototype.getBoundingClientRect = function () { if (this?.getAttribute?.("data-measurement") === "true") { return { width: 900, height: 120, top: 0, left: 0, right: 900, bottom: 120, }; } return originalGetBoundingClientRect.call(this); }; setTimeout(() => { try { const wrapper = document.createElement("div"); wrapper.setAttribute("data-monster-message-layout", "wide"); wrapper.textContent = "wide content placeholder that should grow until the viewport edge"; button.setMessage(wrapper); button.showMessage(); const popper = button.shadowRoot.querySelector( '[data-monster-role="popper"]', ); expect(popper.style.width).to.equal("768px"); expect(popper.style.maxWidth).to.equal("768px"); done(); } catch (e) { done(e); } }, 0); }); }); }); function waitForCondition(check, { timeout = 4000, interval = 25 } = {}) { return new Promise((resolve, reject) => { const start = Date.now(); const poll = () => { try { if (check()) { resolve(); return; } } catch (error) { reject(error); return; } if (Date.now() - start >= timeout) { reject(new Error("Timed out while waiting for test condition.")); return; } setTimeout(poll, interval); }; poll(); }); }