@mr_hugo/boredom
Version:
Another boring JavaScript framework.
697 lines (569 loc) • 24.3 kB
text/typescript
import "chai/chai.js";
import { expect } from "chai";
import {
fireEvent,
getByLabelText,
getByRole,
getByText,
queryAllByLabelText,
queryByLabelText,
queryByText,
} from "@testing-library/dom";
import "mocha/mocha.js";
import { inflictBoreDOM, webComponent } from "../src/index";
function renderHTML(html: string) {
const main = document.querySelector("main");
if (!main) throw new Error("No <main> found!");
main.innerHTML = html;
return main;
}
async function frame(): Promise<number> {
return new Promise((resolve) => {
requestAnimationFrame((t) => resolve(t));
});
}
async function renderHTMLFrame(html: string): Promise<HTMLElement> {
const main = document.querySelector("main");
if (!main) throw new Error("No <main> found!");
main.innerHTML = html;
return (new Promise((resolve) => {
requestAnimationFrame(() => {
resolve(main);
});
}));
}
export default function () {
describe("DOM", () => {
beforeEach(function () {
const main = document.querySelector("main");
if (!main) return;
main.innerHTML = "";
});
describe("Simple component", () => {
it("should register the <template> data-component tag", async () => {
const container = renderHTML(
`<template data-component="simple-component"></template>`,
);
inflictBoreDOM();
const ctor = customElements.get("simple-component");
expect(ctor).not.to.be.undefined;
if (!ctor) throw new Error("Undefined tag");
expect(new ctor()).to.be.an.instanceof(HTMLElement);
});
it("should not register the <template> data-component tag if it is invalid", async () => {
const container = renderHTML(
`<template data-component="nonvalid"></template>`,
);
inflictBoreDOM();
const ctor = customElements.get("nonvalid");
expect(ctor).to.be.undefined;
});
it("should render the html of the custom element", async () => {
const container = renderHTML(`
<simple-component2></simple-component2>
<template data-component="simple-component2"><p>This is some random HTML</p></template>
`);
inflictBoreDOM();
const elem = getByText(container, "This is some random HTML");
expect(elem).to.be.an.instanceof(HTMLParagraphElement);
});
it("should render in shadow root the html of the corresponding <template> tag when it has shadowmode set", async () => {
const container = renderHTML(`
<simple-component3></simple-component3>
<template data-component="simple-component3" shadowrootmode="open"><p>Test</p></template>
`);
inflictBoreDOM();
const elem = getByText(
(container.firstElementChild as any).shadowRoot,
"Test",
);
expect(elem).to.be.an.instanceof(HTMLParagraphElement);
});
it("should apply the aria attributes from the <template> to the tag of the custom element", async () => {
const container = renderHTML(`
<simple-component4></simple-component4>
<template data-component="simple-component4" data-aria-label="Some Label"><p>Something</p></template>
`);
inflictBoreDOM();
const elem = getByLabelText(container, "Some Label");
expect(elem).to.be.an.instanceof(HTMLElement);
expect(elem.tagName).to.equal("SIMPLE-COMPONENT4");
expect(elem.firstChild).to.be.an.instanceof(HTMLParagraphElement);
});
it("should apply the role attribute from the <template> to the tag of the custom element", async () => {
const container = renderHTML(`
<simple-component5></simple-component5>
<template data-component="simple-component5" data-role="banner"><p>Something</p></template>
`);
inflictBoreDOM();
const elem = getByRole(container, "banner");
expect(elem).to.be.an.instanceof(HTMLElement);
expect(elem.tagName).to.equal("SIMPLE-COMPONENT5");
expect(elem.firstChild).to.be.an.instanceof(HTMLParagraphElement);
});
it("should allow the slots default behaviour", async () => {
const container = await renderHTMLFrame(`
<slotted-component1>
<span slot="my-text">Let's have some different text!</span>
</slotted-component1>
<template data-component="slotted-component1" shadowrootmode="open">
<p><slot name="my-text">My default text</slot></p>
</template>
`);
await inflictBoreDOM(); // replacing slots requires "shadowrootmode" to be set
const elem = getByText(
container,
"Let's have some different text!",
);
expect(elem).to.be.an.instanceof(HTMLElement);
const shouldNotExist = queryByText(
container,
"My default text",
);
expect(shouldNotExist).to.be.null;
});
});
describe("Simple component events", () => {
it("should set a data-event-dispatches on the web component once the custom event is registered", () => {
const container = renderHTML(`
<eventful-component1></eventful-component1>
<template data-component="eventful-component1"><button onclick="dispatch('clickme')">Click me</button></template>
`);
inflictBoreDOM();
const elem = container.querySelector(
"[data-onclick-dispatches]",
) as HTMLElement;
expect(elem).to.be.an.instanceof(HTMLElement);
expect(elem.dataset.onclickDispatches).to.eql("clickme");
});
it("should dispatch a custom event with the provided name in the dispatch function", async (done) => {
const container = renderHTML(`
<eventful-component2></eventful-component2>
<template data-component="eventful-component2"><button onclick="dispatch('clickme')">Click me</button></template>
`);
inflictBoreDOM();
addEventListener("clickme", (e: any) => {
expect(e.detail.event).not.to.be.undefined;
expect(e.detail.event.target).to.be.an.instanceof(HTMLElement);
if (!(e.detail.event.target instanceof HTMLElement)) {
throw new Error("Event target not an html element");
}
expect(e.detail.event.target.tagName.toLowerCase()).to.equal(
"button",
);
done();
});
const elem = getByText(
container,
"Click me",
);
fireEvent.click(elem);
});
it("should dispatch more than one custom event when more than one string is in the dispatch function", async (done) => {
const container = renderHTML(`
<eventful-component3></eventful-component3>
<template data-component="eventful-component3"><button onclick="dispatch('clickyou', 'clickthem')">Click me</button></template>
`);
inflictBoreDOM();
let triggeredEvents: string[] = [];
addEventListener("clickthem", (e: any) => {
expect(e.detail.event).not.to.be.undefined;
expect(e.detail.event.target).to.be.an.instanceof(HTMLElement);
if (!(e.detail.event.target instanceof HTMLElement)) {
throw new Error("Event target not an html element");
}
expect(e.detail.event.target.tagName.toLowerCase()).to.equal(
"button",
);
triggeredEvents.push("clickthem");
if (triggeredEvents.includes("clickyou")) {
done();
}
});
addEventListener("clickyou", (e: any) => {
expect(e.detail.event).not.to.be.undefined;
expect(e.detail.event.target).to.be.an.instanceof(HTMLElement);
if (!(e.detail.event.target instanceof HTMLElement)) {
throw new Error("Event target not an html element");
}
expect(e.detail.event.target.tagName.toLowerCase()).to.equal(
"button",
);
triggeredEvents.push("clickyou");
if (triggeredEvents.includes("clickthem")) {
done();
}
});
const elem = getByText(
container,
"Click me",
);
// One click, should trigger two custom events
fireEvent.click(elem);
});
});
describe("Component with <script> code", () => {
it("should load the associated JS and run the render function", async () => {
// The following code is accompanied by the `stateful-component1.js` file.
const container = renderHTML(`
<stateful-component1></stateful-component1>
<template data-component="stateful-component1">
<p>Stateful component 1</p>
</template>
<script src="/stateful-component1.js"></script>
`);
// The `stateful-component1.js` should be automatically imported dynamically and
// its render function called.
await inflictBoreDOM();
const elem = getByText(
container,
"Render",
);
expect(elem).to.be.an.instanceof(HTMLParagraphElement);
});
it("should pass refs through an object in init", async () => {
// The following code is accompanied by the `stateful-component2.js` file.
const container = await renderHTMLFrame(`
<stateful-component2></stateful-component2>
<template data-component="stateful-component2">
<p>Some ref:
<span data-ref="something"> </span> </p>
<!-- ^ should be available as options.refs.something in the init function -->
</template>
<script src="/stateful-component2.js"></script>
`);
await inflictBoreDOM(); // Runs the code in `stateful-component2.js`
const elem = getByText(
container,
"Something ref innerText updated",
);
expect(elem).to.be.an.instanceof(HTMLSpanElement);
});
it("should throw an error when an undefined ref is being accessed", async () => {
// The following code is accompanied by the `stateful-component3.js` file.
const container = await renderHTMLFrame(`
<stateful-component3></stateful-component3>
<template data-component="stateful-component3"></template>
<script src="/stateful-component3.js"></script>
`);
try {
await inflictBoreDOM(); // Runs the code in `stateful-component3.js`
} catch (e) {
expect((e as Error).message).to.be.a.string(
'Ref "somethingThatDoesNotExist" not found in <STATEFUL-COMPONENT3>',
);
}
});
it("should be able to get slots through the `slots` object property in the render function", async () => {
// The following code is accompanied by the `stateful-component4.js` file.
const container = await renderHTMLFrame(`
<stateful-component4></stateful-component4>
<template data-component="stateful-component4">
<p>Something can be placed below:</p>
<slot name="some-slot"></slot>
<!-- ^ should be available as options.slots["some-slot"] in the render function -->
</template>
<script src="/stateful-component4.js"></script>
<template data-component="stateful-component4b">
<p>This component will be placed in the slot by the .js code</p>
</template>
`);
await inflictBoreDOM(); // Runs the code in `stateful-component4.js`
const elem = getByText(
container,
"This component will be placed in the slot by the .js code",
);
expect(elem).to.be.an.instanceof(HTMLParagraphElement);
});
it("should be able to set slots through the `slots` object property and replace the slot element", async () => {
// The following code is accompanied by the `stateful-component5.js` file.
const container = await renderHTMLFrame(`
<stateful-component5></stateful-component5>
<template data-component="stateful-component5">
<p>Something can be placed below:</p>
<slot name="some-slot">This will be replaced</slot>
<!-- ^ should be available to be replaced by setting options.slots["some-slot"] in the render function -->
</template>
<script src="/stateful-component5.js"></script>
`);
await inflictBoreDOM(); // Runs the code in `stateful-component5.js`
const replaced = queryByText(
container,
"This will be replaced",
);
expect(replaced).to.be.null;
const elem = getByText(
container,
"Text in a paragraph that replaced the slot",
);
expect(elem).to.be.an.instanceof(HTMLParagraphElement);
});
it("should place the slot name in a data attribute of the element that replaces it", async () => {
// The following code is accompanied by the `stateful-component5.js` file.
const container = await renderHTMLFrame(`
<stateful-component5></stateful-component5>
<template data-component="stateful-component5">
<p>Something can be placed below:</p>
<slot name="some-slot">This will be replaced</slot>
<!-- ^ should be available to be replaced by setting options.slots["some-slot"] in the render function -->
</template>
<script src="/stateful-component5.js"></script>
`);
await inflictBoreDOM(); // Runs the code in `stateful-component5.js`
const elem = getByText(
container,
"Text in a paragraph that replaced the slot",
);
expect(elem.dataset.slot).to.be.string(
"some-slot",
"Should have a `data-slot='slot-name' attribute`",
);
});
it("should allow script code to be defined in the `inflictBoreDOM()` function", async () => {
const container = renderHTML(`
<inline-component1></inline-component1>
<template data-component="inline-component1">
<p>Stateful inline component 1</p>
</template>
<!-- code will be set in inflictBoreDOM -->
`);
await inflictBoreDOM(undefined, {
"inline-component1": webComponent(() => ({ self }) => {
self.innerHTML = "Inline code run";
}),
});
const elem = getByText(
container,
"Inline code run",
);
expect(elem).to.be.an.instanceof(HTMLElement);
expect(elem.tagName).to.be.equals("INLINE-COMPONENT1");
});
it("should initialize all instances of the same component", async () => {
const container = await renderHTMLFrame(`
<multi-instance-component></multi-instance-component>
<multi-instance-component></multi-instance-component>
<template data-component="multi-instance-component">
<p>Multi instance component</p>
</template>
<script src="/multi-instance-component.js"></script>
`);
await inflictBoreDOM();
const instances = Array.from(
container.querySelectorAll("multi-instance-component"),
);
expect(instances.length).to.equal(2);
expect(instances[0]).to.be.an.instanceof(HTMLElement);
expect(instances[1]).to.be.an.instanceof(HTMLElement);
// Both instances should have been initialized with their index
expect(instances[0].getAttribute("data-index")).to.equal("0");
expect(instances[1].getAttribute("data-index")).to.equal("1");
});
});
describe("Event handlers in scripts", () => {
it(
"should handle custom events with the provided 'on' function",
function (done) {
(async () => {
// The following code is accompanied by the `stateful-component5.js` file.
const container = await renderHTMLFrame(`
<on-event-component1></on-event-component1>
<template data-component="on-event-component1">
<button onclick="dispatch('someCustomEventOnClick')">Click here to dispatch</butbbon>
</template>
<script src="/on-event-component1.js"></script>
`);
const state = { onDone: done };
await inflictBoreDOM(state);
const elem = getByText(
container,
"Click here to dispatch",
);
// One click, should trigger the custom event, and call the registered callbackes
// provided to the 'on' function (see 'on-event-component1.js')
fireEvent.click(elem);
})();
},
);
it(
"should be able to update the state and automatically render in the provided 'on' function",
async () => {
// The following code is accompanied by the `stateful-component5.js` file.
const container = await renderHTMLFrame(`
<on-event-component2></on-event-component2>
<template data-component="on-event-component2">
<p data-ref="label">Value</p>
<button onclick="dispatch('incrementClick')">Increment</button>
</template>
<script src="/on-event-component2.js"></script>
`);
const state = { value: 0 };
await inflictBoreDOM(state);
// Label should be "0", because the "value" attribute is being set on render:
const labelElem = getByText(
container,
"0",
);
const btn = getByText(
container,
"Increment",
);
// One click, should trigger the custom event, and call the registered callbackes
// provided to the 'on' function (see 'on-event-component1.js')
fireEvent.click(btn);
await frame();
const newLabelElem = getByText(
container,
"1",
);
expect(newLabelElem.innerText).to.be.string("1");
},
);
});
describe("State in component <script> code", () => {
it("should pass the provided state ", async () => {
// The following code is accompanied by the `stateful-component6.js` file.
const container = await renderHTMLFrame(`
<stateful-component6></stateful-component6>
<template data-component="stateful-component6">
<p>Initial state is: <span data-ref="container"></span></p>
</template>
<script src="/stateful-component6.js"></script>
`);
await inflictBoreDOM({ content: { value: "Initial state" } }); // Runs the code in `stateful-component6.js`
const elem = getByText(
container,
"Initial state",
);
expect(elem).to.be.an.instanceof(HTMLSpanElement);
});
it("should re-render when the provided state has changed", async () => {
// The following code is accompanied by the `stateful-component6.js` file.
const container = await renderHTMLFrame(`
<stateful-component6></stateful-component6>
<template data-component="stateful-component6">
<p>Initial state is: <span data-ref="container"></span></p>
</template>
<script src="/stateful-component6.js"></script>
`);
const state = { content: { value: "Initial state" } };
await inflictBoreDOM(state); // Runs the code in `stateful-component6.js`
// Update the state:
state.content.value = "This is new content";
await frame();
const elem = getByText(
container,
"This is new content",
);
expect(elem).to.be.an.instanceof(HTMLSpanElement);
});
it("should re-render when an array changed in the provided state", async () => {
// The following code is accompanied by the `stateful-component6.js` file.
const container = await renderHTMLFrame(`
<stateful-component7></stateful-component7>
<template data-component="stateful-component7">
<p>Initial state is: <span data-ref="container"></span></p>
</template>
<script src="/stateful-component7.js"></script>
`);
const state = { content: { value: ["Initial state"] } };
await inflictBoreDOM(state); // Runs the code in `stateful-component7.js`
// Update the state:
state.content.value[0] = "This is new content";
await frame();
const elem = getByText(
container,
"This is new content",
);
expect(elem).to.be.an.instanceof(HTMLSpanElement);
});
it("should re-render when an array changed in an event handler", async () => {
// The following code is accompanied by the `stateful-component6.js` file.
const container = await renderHTMLFrame(`
<stateful-component8></stateful-component8>
<template data-component="stateful-component8">
<button onclick="dispatch('update')">Click to update</button>
<p>Initial state is: <span data-ref="container"></span></p>
</template>
<script src="/stateful-component8.js"></script>
`);
const state = { content: { value: ["Initial state"] } };
await inflictBoreDOM(state); // Runs the code in `stateful-component8.js`
const btn = getByText(
container,
"Click to update",
);
fireEvent.click(btn);
await frame();
const elem = getByText(
container,
"This is new content",
);
expect(elem).to.be.an.instanceof(HTMLSpanElement);
});
});
describe("Lists of components in <script> code", () => {
it("should be able to dynamically create a component with a detail object", async () => {
// The following code is accompanied by the `list-component1.js` file.
const container = await renderHTMLFrame(`
<list-component1></list-component1>
<template data-component="list-component1">
<p>Below will be added a dynamic component</p>
<ol>
</ol>
</template>
<script src="/list-component1.js"></script>
<template data-component="list-item1">
<li></li>
</template>
<script src="/list-item1.js"></script>
`);
await frame();
await inflictBoreDOM({ content: { items: ["some item"] } }); // Runs the code in `list-component1.js`
const elem = getByText(
container,
"some item",
);
expect(elem).to.be.an.instanceof(HTMLElement);
});
it("should dynamically create multiple components", async () => {
// The following code is accompanied by the `list-component1.js` file.
// This is the same as the previous test
const container = await renderHTMLFrame(`
<list-component1></list-component1>
<template data-component="list-component1">
<p>Below will be added a dynamic component</p>
<ol>
</ol>
</template>
<script src="/list-component1.js"></script>
<template data-component="list-item1">
<li></li>
</template>
<script src="/list-item1.js"></script>
`);
await frame();
// In this test, pass multiple items in the array
await inflictBoreDOM({
content: { items: ["item A", "item B", "item C"] },
});
// ^ Runs the code in `list-component1.js`
const elem1 = getByText(
container,
"item A",
);
const elem2 = getByText(
container,
"item B",
);
const elem3 = getByText(
container,
"item C",
);
expect(elem1).to.be.an.instanceof(HTMLElement);
expect(elem2).to.be.an.instanceof(HTMLElement);
expect(elem3).to.be.an.instanceof(HTMLElement);
});
});
});
}