Recently I have been using lit, a web component framework.
If you are not familiar with web components here is a quick summary:
Web Components is a suite of different technologies allowing you to create reusable custom elements - with their functionality encapsulated away from the rest of your code - and utilize them in your web apps.
MDN - https://developer.mozilla.org/en-US/docs/Web/Web_Components
Lit is a popular framework for these components with over 1 million weekly downloads. (Oct 10th 2022).
Testing Lit
For how to test Lit Elements I had been following some big notable projects,
Spectrum web components by Adobe is really mature design system that makes a lot of usage of Lit Elements. Their testing setup uses the suggested web test runner. Lit's documentation on testing suggests using that library.
Cypress has become one of the most popular test runners in the space. So I was a little surprised to not see some guidance for it on the lit documentation. I think it's an ideal candidate for Web Component testing on modern browsers, and because it recently added support for safari it has great coverage across chromium, webkit, electron and quantum (firefox) based browsers.
Testing Lit Components with Cypress
To test with lit elements you will need a project with cypress.
TL;DR
An example project: https://github.com/simonireilly/cypress-lit
The testing setup consists of writing a cy.mount
command for mounting Lit Elements into the DOM.
import { getContainerEl } from "@cypress/mount-utils";
import { LitElement, render, TemplateResult } from "lit";
export const mount = (_element: LitElement, template: TemplateResult) => {
const target = getContainerEl();
render(template, target);
return cy
.wait(0, { log: false })
.then(() => {
const mountMessage = `
<${String(_element.constructor.name)} ... />
`;
Cypress.log({
name: "mount",
message: [mountMessage],
})
.snapshot("mounted")
.end();
})
.get("[data-cy-root]")
.children()
.first()
.shadow();
};
And this means you can write test like this:
import { html } from "lit";
import { MyElement } from "../../src/my-element";
describe("my-element.cy.ts", () => {
it("playground", () => {
cy.mount(new MyElement(), html`<my-element></my-element>`).as("element");
cy.get("@element").contains("count").click();
});
});
And just like that, you can run a cypress test for the component. Here is a quick snapshot of me testing one.
Wrap up
That is a quick example of how to run component tests for Lit with cypress. Hopefully it's just enough information for you to get it setup.
Since cypress components supports so many frameworks already, I don't think it will be long before an official lit component implementation lands. In the meantime, happy testing!
Top comments (2)
Very nice! This is awesome to see!!
I think this doesn't necessarily need to be Lit specific since really all web components can be tested the same way, similar to how the open-wc test fixtures work open-wc.org/docs/testing/helpers/#...
Perhaps the
mount
command can just take either a string or a Lit template result. Passing in the component instance feels not necessary. If we're assuming users will pass in a single custom element, we can grab the first child of the container and get its.tagName
property for the logging.I need to look more into Cypress APIs on how it works with promises but if we're working with Lit elements, awaiting on the component's
updateComplete
promise could be enough to ensure the component is mounted, or perhaps waiting on arequestAnimationFrame
, instead of thecy.wait(0)
.Also, last nit would be that I'd probably like the mount command to return the custom element and not its shadow root by default. Users might want to test against the component's attributes or properties, and probably would intentionally dive into its shadow DOM.
Really appreciate your comments on this one. Would you consider adding them here as I've opened a PR for cypress
github.com/cypress-io/cypress/pull...
I found that without passing the Element through to the Mount helper there was no instance on the DOM (customElementRegistry). There is likely a work around, I agree that supporting all web components would be the best global conclusion.
Yes to the tagName point, much preferred 👍
Your point on the promises to await is really neat, and not something I've considered. Would be really cool if you can contribute this to the PR, or provide feedback if I try and implement it.
Final point I also agree with. It seems a little opaque to return the shadow root.
Summary; really appreciate the feedback 👍