@schukai/monster
Version:
Monster is a simple library for creating fast, robust and lightweight websites.
546 lines (477 loc) • 14.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();
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();
});
}