13

Anyone has a good setup for testing custom elements with jest, jsdom or similar? I have been using Puppeteer and Selenium, but they slow down the test runs too much. Any other alternatives or fixes for jsdom that makes the below test runnable?

import {LitElement} from 'lit-element';
import {html} from 'lit-html';

export class Counter extends LitElement {
  static get properties() {
    return Object.assign({}, super.properties, {
      count: {type: Number}
    });
  }

  render() {
    return html`Count is ${this.count}`;
  }
}

customElements.define('c-counter', Counter);

With the test file:

import './counter';

describe('c-counter', () => {
  it('should be registered', () => {
    expect(customElements.get('c-counter')).toBeDefined();
  });

  it('render', async () => {
    const element = window.document.createElement('c-counter');
    window.document.body.appendChild(element);

    await element.updateComplete;
    expect(element.innerHTML).toContain('Count is undefined');

    element.count = 3;
    await element.updateComplete;

    expect(element.innerHTML).toContain('Count is 3');
  });
});

And finally this is the current jest environment setup:

const {installCommonGlobals} = require('jest-util');
const {JSDOM, VirtualConsole} = require('jsdom');
const JSDOMEnvironment = require('jest-environment-jsdom');
const installCE = require('document-register-element/pony');

class JSDOMCustomElementsEnvironment extends JSDOMEnvironment {
  constructor(config, context) {
    super(config, context);

    this.dom = new JSDOM('<!DOCTYPE html>', {
      runScripts: 'dangerously',
      pretendToBeVisual: true,
      VirtualConsole: new VirtualConsole().sendTo(context.console || console),
      url: 'http://jsdom'
    });

    /* eslint-disable no-multi-assign */
    const global = (this.global = this.dom.window.document.defaultView);

    installCommonGlobals(global, config.globals);

    installCE(global.window, {
      type: 'force',
      noBuiltIn: false
    });
  }

  teardown() {
    this.global = null;
    this.dom = null;

    return Promise.resolve();
  }
}

module.exports = JSDOMCustomElementsEnvironment;
Tushar Shukla
  • 5,666
  • 2
  • 27
  • 41
tirithen
  • 3,219
  • 11
  • 41
  • 65
  • It'd be helpful if you shared what the error is. And since so few people, if any, are running web component tests in Jest, I'd probably try to get a reproduction up in a repo or gist for people to try. – Justin Fagnani Mar 14 '19 at 18:01
  • I don't expect this to be great until jsdom supports custom elements and JS modules. Given the large number of DOM APIs that web components use, I think testing in a real browser is the much more prudent approach. You should test in all the actual browsers that you support. – Justin Fagnani Mar 14 '19 at 18:02
  • 1
    I fully agree with Justin on this one... if you need any help with setting up testing with real browser take a look here https://open-wc.org/testing/ - that should be pretty straight forward and gives you most of the features you know from Jest in a real browser – daKmoR Apr 02 '19 at 18:39
  • @JustinFagnani, I am looking for a good stack for testing vanilla webcomponents (you will find several questions from me regard this topic over here). You said "few people running web component test in Jest". Do you know the pro/against use Jest for testing webcomponents? Don't get me wrong but simply saying "few are using..." doesn't help much (you will find many people saying that no many peolpe use web-components. I have use Polymer with considedrable success and now I am trying vanilla web-components). "Few/many/numerous/etc doesn't add much but if you can shortly point why not it will – Jim C Jul 11 '19 at 16:36
  • I am experiencing problems with this myself – Josh May 17 '20 at 03:43
  • @Josh The project where I used this is moving towards using the open-wc.org instead as that project is active and has testing with coverage setup out of the box and still uses LitElement for the web components. That is probably a better way to do testing with LitElement. – tirithen May 20 '20 at 13:31

1 Answers1

5

It is possible with a bit of additional setup.

If you look at open wc docs, they recommend testing your web components in the browser which they already do with Karma and Headless Chrome. As you already pointed out Puppeteer and Selenium are too slow for this and the only viable browser alternative is ElectronJS. There is a runner available for Jest.

https://github.com/hustcc/jest-electron

This will allow you to render your web components in a real browser with access to Shadow DOM and your tests will still be fast. Something like this, I use Webpack for processing my code.

// button.ts
import {html, customElement, LitElement, property} from "lit-element";

@customElement('awesome-button')
export class Button extends LitElement {

    @property()
    buttonText = '';

    render() {
        return html`<button id="custom-button"
            @click="${() => {}}">${this.buttonText}</button>`;
    }
}

Webpack configuration

const path = require('path');
const {CleanWebpackPlugin} = require('clean-webpack-plugin');

module.exports = {
    mode: 'development',
    entry: './index.ts',
    module: {
        rules: [
            {
                test: /\.ts?$/,
                use: 'ts-loader',
                exclude: /node_modules/,
            },
        ],
    },
    resolve: {
        extensions: ['.tsx', '.ts', '.js'],
    },
    plugins: [
        new CleanWebpackPlugin()
    ],
    output: {
        filename: 'main.js',
        path: path.resolve(__dirname, 'dist'),
    },
};

Jest configuration

module.exports = {
    preset: 'ts-jest',
    runner: 'jest-electron/runner',
    testEnvironment: 'jest-electron/environment',
    setupFiles: ['./dist/main.js'],
};

And finally our test.

import {LitElement} from 'lit-element';

describe('awesome-button', () => {

    const AWESOME_BUTTON_TAG = 'awesome-button';
    const ELEMENT_ID = 'custom-button';
    let buttonElement: LitElement;

    const getShadowRoot = (tagName: string): ShadowRoot => {
        return document.body.getElementsByTagName(tagName)[0].shadowRoot;
    }

    beforeEach(() => {
        buttonElement = window.document.createElement(AWESOME_BUTTON_TAG) as LitElement;
        document.body.appendChild(buttonElement);
    });

    afterEach(() => {
       document.body.getElementsByTagName(AWESOME_BUTTON_TAG)[0].remove();
    });

    it('displays button text', async () => {
        const dummyText = 'Web components & Jest with Electron';
        buttonElement.setAttribute('buttonText', dummyText);
        await buttonElement.updateComplete;

        const renderedText = getShadowRoot(AWESOME_BUTTON_TAG).getElementById(ELEMENT_ID).innerText;

        expect(renderedText).toEqual(dummyText);
    });
    it('handles clicks', async () => {
        const mockClickFunction = jest.fn();
        buttonElement.addEventListener('click', () => {mockClickFunction()});

        getShadowRoot(AWESOME_BUTTON_TAG).getElementById(ELEMENT_ID).click();
        getShadowRoot(AWESOME_BUTTON_TAG).getElementById(ELEMENT_ID).click();

        expect(mockClickFunction).toHaveBeenCalledTimes(2);
    });
});

I even wrote a blog post about this and there you can find repos with the full setup etc.

https://www.ninkovic.dev/blog/2020/testing-web-components-with-jest-and-lit-element

leonheess
  • 16,068
  • 14
  • 77
  • 112
Wizard
  • 292
  • 4
  • 11
  • How do I add code coverage logs, like karma in this? – Matheus Ribeiro Jun 29 '21 at 14:21
  • @MatheusRibeiro Jest has plenty of options for setting up code coverage with different reporters. Please see the documentation. https://jestjs.io/docs/configuration#collectcoverage-boolean – Wizard Jul 01 '21 at 00:26