The problem
Recently one of my pet projects - a web component that mimics, localized and extends the default <input>
tag - had gotten complex enough that I couldn't warrant manual testing for it anymore. So, naturally, I tried to setup a unit testing environment. JSDom supports custom elements, right? Well yeah, kinda. Problem is, my little component wants to integrate with ElementInternals
so plays nicely with forms. And JSDom hasn't implemented that at the time of writing. And with that I opted for puppeteer.
So if you find yourself reading this article at a point in time where JSDom has implemented ElementInternals
I'd suggest to stop reading and try it with JSDom instead.
The basics
In a nutshell, all we have to do is startup a new page with puppeteer, fill it with some skeleton that includes our library and then add our web component to the body of that page, after which we can query for that element and assert it behaves and looks like we expect it to.
The requirements
Since I don't exactly like writing html strings by hand and jsx is such a nice templating language, I've used another library of mine to transform typed jsx into html strings.
So with a quick
npm install -D tsx-to-html puppeteer
we should have everything we need
The setup
A simple function to return us a page with our content should be enough.
// Utils/Test.tsx
import puppeteer from "puppeteer";
import { toHtml } from "tsx-to-html";
import * as Fs from "node:fs";
const lib = Fs.readFileSync("dist/index.js", "utf8");
const browser = await puppeteer.launch({ headless: "new" });
export const init = async (content: JSX.Element) => {
const consoleMessages = [] as ConsoleMessage[];
const page = Object.assign(await browser.newPage(), {
console: { messages: consoleMessages },
});
page.on("console", async (msg) => {
consoleMessages.push(msg);
const args = await Promise.all(msg.args().map((a) => a.jsonValue()));
console.log(...args);
});
page.on("pageerror", console.error);
await page.setContent(
toHtml(
<html>
<head>
<script type="module">{lib}</script>
<style>{css}</style>
</head>
<body>{content}</body>
</html>,
),
);
return page;
};
All we're doing here is launch a new headless chrome, create a page in it, include our (preloaded) script (type="module"
is necessary for me because I'm outputting ESM) and include the passed content
in its body.
The tests
import { describe, it, expect } from "yourFavoriteTestrunner";
import { init } from "Utils/Test";
describe("uwc-input", () => {
it("should have class pristine", async () => {
const page = await init(<uwc-input></uwc-input>);
const classes = await page.$eval("uwc-input", (input) =>
Array.from(input.classList),
);
expect(classes).toContain("pristine");
});
});
The importance of types
Don't forget to include your custom element in the HTMLElementTagNameMap
for some nice type safety like this
class MyElem extends HTMLElement {}
declare global {
type MyElem = (typeof MyElem)["prototype"];
interface HTMLElementTagNameMap {
"my-elem": MyElem
}
}
The conclusion
Initially pushing this setup away from me for fear of being too complicated I think it took me longer to write this blog post than to get started with this, so... start testing, will ya?
Top comments (0)