joicomponents
Version:
Design patterns to build native web components
470 lines (434 loc) • 20.5 kB
JavaScript
function makeTestClass(name, shadowString, mixin, slotCallback) {
return class extends mixin(HTMLElement) {
constructor() {
super();
this.attachShadow({mode: "open"});
this.shadowRoot.innerHTML = shadowString;
this.testValue = [];
}
[slotCallback](slot, indirectness, ev) {
if (this._stop) throw new Error("Bug in test: Lingering slotCallback");
this.testValue.push({
slotName: slot.name,
value: slot.assignedNodes({flatten: true}),
slot,
indirectness,
ev
});
}
stop() {
this._stop = true;
}
}
}
function testAssignedValues(actuals, expecteds) {
expect(actuals.length === expecteds.length);
for (let i = 0; i < expecteds.length; i++) {
let exp = expecteds[i];
let act = actuals[i];
expect(act.slot.name).to.be.equal(exp.name);
let assigned = act.value.map(n => n.nodeName ? n.nodeName : "#text");
expect(assigned).to.deep.equal(exp.flattened);
}
}
const runSlotchangeMixinTest = function (SlotchangeMixinType, slotCallback) {
describe("CallBack: " + SlotchangeMixinType.name, function () {
const name = SlotchangeMixinType.name.toLowerCase();
const Slot1Shadow = "<slot></slot>";
const GrandpaSlotShadow = `
<${name}-chained-slot>
<slot></slot>
</${name}-chained-slot>`;
const GrandpaSlotWithSlotnameShadow = `
<${name}-chained-slot>
<slot name="a"></slot>
</${name}-chained-slot>`;
const GrandGrandSlotShadow = `
<${name}-grandpa-slot>
<slot></slot>
</${name}-grandpa-slot>`;
const ChainedSlotsGrandpaErrorShadow = `
<${name}-test-one>
<div>
<slot></slot>
</div>
</${name}-test-one>`;
class SlotWrapper extends HTMLElement {
constructor() {
super();
this.attachShadow({mode: "open"});
this.shadowRoot.innerHTML = `
<${name}-test-one>
<slot></slot>
</${name}-test-one>`;
}
}
const Slot1 = makeTestClass("Slot1", Slot1Shadow, SlotchangeMixinType, slotCallback);
const GrandpaSlot = makeTestClass("GrandpaSlot", GrandpaSlotShadow, SlotchangeMixinType, slotCallback);
const GrandpaSlotWithSlotname = makeTestClass("GrandpaSlotWithName", GrandpaSlotWithSlotnameShadow, SlotchangeMixinType, slotCallback);
const GrandGrandSlot = makeTestClass("GrandGrandSlot", GrandGrandSlotShadow, SlotchangeMixinType, slotCallback);
const ChainedSlotsGrandpaError = makeTestClass("ChainedSlotsGrandpaError", ChainedSlotsGrandpaErrorShadow, SlotchangeMixinType, slotCallback);
customElements.define(name + "-chained-slot-error", ChainedSlotsGrandpaError);
customElements.define(name + "-grand-grand-slot", GrandGrandSlot);
customElements.define(name + "-grandpa-slot-with-name", GrandpaSlotWithSlotname);
customElements.define(name + "-grandpa-slot", GrandpaSlot);
customElements.define(name + "-chained-slot", SlotWrapper);
customElements.define(name + "-test-one", Slot1);
it("extend HTMLElement class correctly and make an element", function () {
class SlotTestClass extends SlotchangeMixinType(HTMLElement) {
constructor() {
super();
this.attachShadow({mode: "open"});
}
[slotCallback]() {
}
}
customElements.define(name + "-slot-test-class", SlotTestClass);
const el = new SlotTestClass();
let proto = el.constructor;
expect(proto.name).to.be.equal("SlotTestClass");
proto = Object.getPrototypeOf(proto);
expect(proto.name).to.be.equal(SlotchangeMixinType.name);
proto = Object.getPrototypeOf(proto);
expect(proto.name).to.be.equal("HTMLElement");
});
it("SlottableMixin add DIV imperative and trigger slotchangedCallback", function (done) {
const el = new Slot1();
el.appendChild(document.createElement("div"));
requestAnimationFrame(() => {
const elExpected = [
{name: "", flattened: ["DIV"]}
];
testAssignedValues(el.testValue, elExpected);
el.stop();
done();
});
});
it("chained slot test", function (done) {
const el = new SlotWrapper();
const inner = el.shadowRoot.children[0];
el.appendChild(document.createElement("div"));
requestAnimationFrame(() => {
const elExpected = [
{name: "", flattened: ["#text", "DIV", "#text"]}
];
testAssignedValues(inner.testValue, elExpected);
inner.stop();
done();
});
});
it("chained slot test: two slotCallbacks", function (done) {
const el = new SlotWrapper();
const inner = el.shadowRoot.children[0];
requestAnimationFrame(() => {
expect(inner.testValue.length).to.be.equal(1);
expect(inner.testValue[0].slotName).to.be.equal("");
expect(inner.testValue[0].value.length).to.be.equal(2);
el.appendChild(document.createElement("p"));
Promise.resolve().then(() => { //we must wait for the slotchange event which is run at the end of microtask que
// if (name === "varmixin") //not called, as SlotWrapper doesn't implement VarMixin, but only has a regular slot. Therefore, the top, initial callback doesn't trigger.
// return done();
expect(inner.testValue.length).to.be.equal(2);
expect(inner.testValue[1].slotName).to.be.equal("");
expect(inner.testValue[1].value.length).to.be.equal(3);
expect(inner.testValue[1].value[1].nodeName).to.be.equal("P");
inner.stop();
done();
});
});
});
//the .composedPath() of the slotchange event looks like this:
//[slot, div, slot, document-fragment, shadowslotchangemixinarnold-test-one, document-fragment]
//here, the "div" between the two slots indicate that the direct children of the slot has not changed,
//only a grandchild. Such grandchild slotchange events should not trigger slotchange.
it("not listening for slotChange on slots that are not a direct child", function (done) {
const el = new ChainedSlotsGrandpaError();
const inner = el.shadowRoot.children[0];
el.appendChild(document.createElement("p"));
requestAnimationFrame(() => {
expect(inner.testValue.length).to.be.equal(1);
expect(inner.testValue[0].value.length).to.be.equal(3);
expect(inner.testValue[0].value[1].nodeName).to.be.equal("DIV");
el.appendChild(document.createElement("p"));
expect(inner.testValue.length).to.be.equal(1);
inner.stop();
done();
});
});
it("connected-disconnected is irrelevant. rAF is what counts.", function (done) {
const el = new Slot1();
el.appendChild(document.createElement("div")); //is not triggered.
document.querySelector("body").appendChild(el); //slotchange event is flagged
document.querySelector("body").removeChild(el); //disconnect
el.appendChild(document.createElement("div")); //is not triggered.
el.appendChild(document.createElement("div")); //is not triggered.
document.querySelector("body").appendChild(el); //slotchangedCallback triggered on connect
expect(el.testValue).to.deep.equal([]);
requestAnimationFrame(() => {
const test = [{slotName: "", value: ["DIV", "DIV", "DIV"]}];
expect(el.testValue.length).to.be.equal(1);
expect(el.testValue[0].slotName).to.be.equal("");
expect(el.testValue[0].value.length).to.be.equal(3);
expect(el.testValue[0].value[0].nodeName).to.be.equal("DIV");
expect(el.testValue[0].value[1].nodeName).to.be.equal("DIV");
expect(el.testValue[0].value[2].nodeName).to.be.equal("DIV");
document.querySelector("body").removeChild(el);
el.stop();
done();
});
});
it("connected-wait-disconnected-connected.", function (done) {
const el = new Slot1();
el.appendChild(document.createElement("div")); //slotchangedCallback added to the microque
requestAnimationFrame(() => {
expect(el.testValue.length).to.be.equal(1);
expect(el.testValue[0].value.length).to.be.equal(1);
expect(el.testValue[0].value[0].nodeName).to.be.equal("DIV");
el.appendChild(document.createElement("div")); //slotchangedCallback will be checked at end of microtasks
el.appendChild(document.createElement("div"));
Promise.resolve().then(() => {
expect(el.testValue.length).to.be.equal(2);
expect(el.testValue[1].value.length).to.be.equal(3);
expect(el.testValue[1].value[0].nodeName).to.be.equal("DIV");
expect(el.testValue[1].value[1].nodeName).to.be.equal("DIV");
expect(el.testValue[1].value[2].nodeName).to.be.equal("DIV");
el.stop();
done();
});
});
});
it("Grandpa-slot-test. Simple.", function (done) {
const el = new GrandpaSlot();
const grandChild = el.shadowRoot.children[0].shadowRoot.children[0];
el.appendChild(document.createElement("div")); //slotchangedCallback added to the microque
requestAnimationFrame(() => {
expect(el.testValue.length).to.be.equal(1);
expect(el.testValue[0].oldChildren).to.be.equal(undefined);
expect(el.testValue[0].value.length).to.be.equal(1);
expect(el.testValue[0].value[0].nodeName).to.be.equal("DIV");
expect(grandChild.testValue[0].value.length).to.be.equal(5);
expect(grandChild.testValue[0].value[2].nodeName).to.be.equal("DIV");
el.stop();
grandChild.stop();
done();
});
});
it("GrandGrand-slot-test. Simple.", function (done) {
const el = new GrandGrandSlot();
const child = el.shadowRoot.children[0];
const grandGrandChild = el.shadowRoot.children[0].shadowRoot.children[0].shadowRoot.children[0];
el.appendChild(document.createElement("div")); //slotchangedCallback added to the microque
requestAnimationFrame(() => {
expect(el.testValue.length).to.be.equal(1);
expect(el.testValue[0].oldChildren).to.be.equal(undefined);
expect(el.testValue[0].value.length).to.be.equal(1);
expect(el.testValue[0].value[0].nodeName).to.be.equal("DIV");
expect(child.testValue[0].value.length).to.be.equal(3);
expect(child.testValue[0].value[1].nodeName).to.be.equal("DIV");
expect(grandGrandChild.testValue[0].value.length).to.be.equal(7);
expect(grandGrandChild.testValue[0].value[3].nodeName).to.be.equal("DIV");
el.stop();
child.stop();
grandGrandChild.stop();
done();
});
});
it("Grandpa-slot-test with different slot names.", function (done) {
const el = new GrandpaSlotWithSlotname();
const grandChild = el.shadowRoot.children[0].shadowRoot.children[0];
el.innerHTML = "<div slot='a'></div>"; //slotchangedCallback added to the microque
requestAnimationFrame(() => {
expect(el.testValue.length).to.be.equal(1);
expect(el.testValue[0].value.length).to.be.equal(1);
expect(el.testValue[0].value[0].nodeName).to.be.equal("DIV");
expect(el.testValue[0].slotName).to.be.equal("a");
expect(grandChild.testValue[0].value.length).to.be.equal(5);
expect(grandChild.testValue[0].value[2].nodeName).to.be.equal("DIV");
expect(grandChild.testValue[0].slotName).to.be.equal("");
el.appendChild(document.createElement("span")); //slotchangedCallback added to the microque
Promise.resolve().then(() => {
if (SlotchangeMixinType === SlotchangeMixin) {
expect(el.testValue.length).to.be.equal(1);
expect(grandChild.testValue.length).to.be.equal(1);
}
else if (SlotchangeMixinType === SlottableMixin) {
expect(grandChild.testValue.length).to.be.equal(1);
expect(el.testValue.length).to.be.equal(2);
expect(el.testValue[1].slotName).to.be.equal("");
expect(el.testValue[1].value.length).to.be.equal(1);
expect(el.testValue[1].value[0].nodeName).to.be.equal("SPAN");
}
el.stop();
grandChild.stop();
done();
});
});
});
it("chained slot with different name test.", function (done) {
const el = new SlotWrapper();
const child = el.shadowRoot.children[0];
const div = document.createElement("div");
div.setAttribute("slot", "offside");
child.appendChild(div);
requestAnimationFrame(() => {
if (SlotchangeMixinType === SlotchangeMixin) {
expect(child.testValue.length).to.be.equal(1);
expect(child.testValue[0].value.length).to.be.equal(2);
expect(child.testValue[0].value[0].nodeName).to.be.equal("#text");
expect(child.testValue[0].value[1].nodeName).to.be.equal("#text");
} else if (SlotchangeMixinType === SlottableMixin) {
expect(child.testValue.length).to.be.equal(2);
expect(child.testValue[0].slotName).to.be.equal("");
expect(child.testValue[0].value.length).to.be.equal(2);
expect(child.testValue[0].value[0].nodeName).to.be.equal("#text");
expect(child.testValue[0].value[1].nodeName).to.be.equal("#text");
expect(child.testValue[1].slotName).to.be.equal("offside");
expect(child.testValue[1].value.length).to.be.equal(1);
expect(child.testValue[1].value[0].nodeName).to.be.equal("DIV");
}
child.stop();
done();
});
});
it("Slot without shadowRoot is added.", function (done) {
const el = new Slot1();
el.appendChild(document.createElement("div"));
el.appendChild(document.createElement("slot"));
requestAnimationFrame(() => {
expect(el.testValue.length).to.be.equal(1);
expect(el.testValue[0].slotName).to.be.equal("");
expect(el.testValue[0].value.length).to.be.equal(2);
expect(el.testValue[0].value[0].nodeName).to.be.equal("DIV");
expect(el.testValue[0].value[1].nodeName).to.be.equal("SLOT");
let alternateFlattened = flattenAssignedNodesVar(el.testValue[0].slot);
expect(alternateFlattened.length).to.be.equal(1);
expect(alternateFlattened[0].nodeName).to.be.equal("DIV");
el.stop();
done();
});
});
it("Slot without shadowRoot is added: GrandPa.", function (done) {
const el = new GrandpaSlot();
const grandChild = el.shadowRoot.children[0].shadowRoot.children[0];
el.appendChild(document.createElement("div"));
el.appendChild(document.createElement("slot"));
requestAnimationFrame(() => {
expect(el.testValue.length).to.be.equal(1);
expect(el.testValue[0].slotName).to.be.equal("");
expect(el.testValue[0].value.length).to.be.equal(2);
expect(el.testValue[0].value[0].nodeName).to.be.equal("DIV");
expect(el.testValue[0].value[1].nodeName).to.be.equal("SLOT");
let alternateFlattened = flattenAssignedNodesVar(el.testValue[0].slot);
expect(alternateFlattened.length).to.be.equal(1);
expect(alternateFlattened[0].nodeName).to.be.equal("DIV");
expect(grandChild.testValue.length).to.be.equal(1);
expect(grandChild.testValue[0].value.length).to.be.equal(6);
expect(grandChild.testValue[0].value[2].nodeName).to.be.equal("DIV");
expect(grandChild.testValue[0].value[3].nodeName).to.be.equal("SLOT");
alternateFlattened = flattenAssignedNodesVar(grandChild.testValue[0].slot);
expect(alternateFlattened.length).to.be.equal(5);
expect(alternateFlattened[2].nodeName).to.be.equal("DIV");
el.stop();
done();
});
});
it("slottables also trigger callbacks in SlottableMixin", function (done) {
const el = new Slot1();
el.innerHTML = "<div id='a'></div><div id='b' slot='something'></div>";
requestAnimationFrame(() => {
if (SlotchangeMixin === SlotchangeMixinType)
return done();
expect(el.testValue.length).to.be.equal(2);
expect(el.testValue[0].slotName).to.be.equal("");
expect(el.testValue[0].value.length).to.be.equal(1);
expect(el.testValue[0].value[0].id).to.be.equal("a");
expect(el.testValue[1].slotName).to.be.equal("something");
expect(el.testValue[1].value.length).to.be.equal(1);
expect(el.testValue[1].value[0].id).to.be.equal("b");
el.stop();
done();
});
});
it("removing elements also trigger callbacks", function (done) {
const el = new Slot1();
el.innerHTML = "<div id='a'></div>";
requestAnimationFrame(() => {
expect(el.testValue.length).to.be.equal(1);
expect(el.testValue[0].slotName).to.be.equal("");
expect(el.testValue[0].value.length).to.be.equal(1);
expect(el.testValue[0].value[0].id).to.be.equal("a");
el.children[0].remove();
requestAnimationFrame(() => {
expect(el.testValue.length).to.be.equal(2);
expect(el.testValue[1].slotName).to.be.equal("");
expect(el.testValue[1].value.length).to.be.equal(0);
el.stop();
done();
});
});
});
it("removing elements also trigger callbacks, slottables", function (done) {
const el = new Slot1();
el.innerHTML = "<div id='a'></div><div id='b' slot='something'></div>";
requestAnimationFrame(() => {
if (SlotchangeMixin === SlotchangeMixinType)
return done();
expect(el.testValue.length).to.be.equal(2);
expect(el.testValue[0].slotName).to.be.equal("");
expect(el.testValue[0].value.length).to.be.equal(1);
expect(el.testValue[0].value[0].id).to.be.equal("a");
expect(el.testValue[1].slotName).to.be.equal("something");
expect(el.testValue[1].value.length).to.be.equal(1);
expect(el.testValue[1].value[0].id).to.be.equal("b");
el.children[1].remove();
requestAnimationFrame(() => {
expect(el.testValue.length).to.be.equal(3);
expect(el.testValue[2].slotName).to.be.equal("something");
expect(el.testValue[2].value.length).to.be.equal(0);
el.children[0].remove();
requestAnimationFrame(() => {
expect(el.testValue.length).to.be.equal(4);
expect(el.testValue[3].slotName).to.be.equal("");
expect(el.testValue[3].value.length).to.be.equal(0);
el.stop();
done();
});
});
});
});
it("removing elements also trigger callbacks, slottables, two at the same time", function (done) {
const el = new Slot1();
el.innerHTML = "<div id='a'></div><div id='b' slot='something'></div>";
requestAnimationFrame(() => {
if (SlotchangeMixin === SlotchangeMixinType)
return done();
expect(el.testValue.length).to.be.equal(2);
expect(el.testValue[0].slotName).to.be.equal("");
expect(el.testValue[0].value.length).to.be.equal(1);
expect(el.testValue[0].value[0].id).to.be.equal("a");
expect(el.testValue[1].slotName).to.be.equal("something");
expect(el.testValue[1].value.length).to.be.equal(1);
expect(el.testValue[1].value[0].id).to.be.equal("b");
el.children[0].remove();
el.children[0].remove();
requestAnimationFrame(() => {
expect(el.testValue.length).to.be.equal(4);
expect(el.testValue[2].slotName).to.be.equal("");
expect(el.testValue[2].value.length).to.be.equal(0);
expect(el.testValue[3].slotName).to.be.equal("something");
expect(el.testValue[3].value.length).to.be.equal(0);
el.stop();
done();
});
});
});
//todo add test for the empty initial child still gets a callback
//todo test for fallback slot nodes
});
};
import {SlotchangeMixin} from "../../../src/slot/SlotchangeMixin.js";
import {SlottableMixin} from "../../../src/slot/SlottableMixin.js";
import {VarMixin, flattenAssignedNodesVar} from "../../../src/slot/VarMixin.js";
runSlotchangeMixinTest(SlotchangeMixin, "slotchangeCallback");
runSlotchangeMixinTest(SlottableMixin, "slottablesCallback");
// runSlotchangeMixinTest(VarMixin, "slotCallback");