Testing Web Components with Karma and TypeScript
Tags: litelement, typescript
Published: 2020-12-01 22:44:53

---
title: Testing Web Components with Karma and TypeScript
published: true
date: 2020-12-01 17:56:07 UTC
tags: litelement,typescript
canonical_url: https://www.thisdot.co/blog/testing-web-components-with-karma-and-typescript
---

Imagine yourself building an electric car.

By design, this car has an electric motor, a cooling system, a battery, a charge port, a transmission, and other **units**. Every part of the system has requirements that must be met before being attached to the other components.

If any of these components has not been properly tested, the car may not work.

Similarly, software can be considered as a collection of units of functionality. The quality of the entire system can be determined by the quality of every part of which its made.

### Unit Tests

Unit tests are functions implemented to verify the behavior of the code under certain scenarios. Let's suppose we have a function that returns a greeting:

```
function greeting(name?: string): string {
  return name? `Hello ${name}!`: 'Hello!';
}
```

Following the TDD approach, you can write your first test with a valid `name='Luis'` as an input with `Hello Luis!` expected as a result.

However, how could you ensure the correct operation of this function under other parameters? Let's think in the following cases:

- An invalid situation, where the function receives an empty string or `undefined`.
- A valid situation, where the function receives a valid name as a string.

Both conditions can be considered part of the same scenario. Then, you may define a formal way to write them as:

```
  - Scenario: "Greeting Function"
    - assert(greeting() == 'Hello!')
    - assert(greeting('Luis') == 'Hello Luis!')
```

In the JavaScript world, there are a lot of options in terms of tools to implement this kind of test. Let's talk about them in the next section.

### Testing Tools for Web Components

Think about the steps you need to perform before you start running unit tests in your JavaScript/TypeScript project.

First, you probably have to think about installing and configuring some tools:

- **A Testing Framework** , which defines a set of guidelines and rules to implement your _test cases_.
- **A Test Runner** , which is the tool that executes your unit tests as a whole suite.

Not only that, it would be great to consider a _Web Test Runner_ to run the JavaScript code and "render" the DOM elements. This is particularly important when testing our web components.

With so many options in mind, it would be easy to lose your way and run out of time. Thus, the [open-wc](https://open-wc.org/) team provides a set of tools and recommendations to facilitate testing tasks.

#### Setup

You can use the [project scaffolding tool](https://github.com/open-wc/open-wc) to create a new project from scratch. The TypeScript support and common tooling for tests is only one command away:

```
npm init @open-wc
# Select "Scaffold a new project" (What would you like to do today?)
# Select "Application" (What would you like to scaffold?)
# Mark/Select "Linting", "Testing", "Demoing" and "Building" (What would you like to add?)
# Yes (Would you like to use TypeScript?)
# Mark/Select "Testing", "Demoing" and "Building" (Would you like to scaffold examples files for?)
# my-project (What is the tag name of your application/web component?)
# Yes (Do you want to write this file structure to disk?)
# Yes, with npm (Do you want to install dependencies?)
```

Next, pay attention to the generated files:

```
|- my-project/
    |- src/
    |- test/
        |- my-project.test.ts
    |- karma.conf.js
    |- package.json
    |- tsconfig.json
    |... other files/folders
```

As you can see, there is a `test` folder where we can add our testing files. Just make sure to use the `.test.ts` extension for them.

The `karma.conf.js` file contains the configuration needed for testing with [Karma](https://karma-runner.github.io/). Why you should use Karma? In the words of the `open-wc` team:

> We recommend karma as a general-purpose tool for testing code that runs in the browser. Karma can run a large range of browsers, including IE11. This way, you are confident that your code runs correctly in all supported environments.

Also, the `package.json` file defines the following scripts for testing:

```
{
  "scripts": {
    ...
    "test": "tsc && karma start --coverage",
    "test:watch": "concurrently --kill-others --names tsc,karma \"npm run tsc:watch\" \"karma start --auto-watch=true --single-run=false\"",
  },
  ...
}
```

Use the `npm run test` command for single running, and use `npm run test:watch` for running unit tests in _watch mode_(Enable watching files and executing the tests whenever one of these file changes).

#### Testing Libraries

The [open-wc](https://open-wc.org) team recommends the following libraries for testing:

- [mocha](https://mochajs.org/), as the testing framework running on Node.js and in the browser.
- [chai](https://www.chaijs.com/), as the assertion library. It can be used with any other JavaScript framework today.

Along with them, there are a set of tools and helpers available:

- [@open-wc/testing-helpers](https://open-wc.org/testing/testing-helpers.html): A library with helpers functions for testing in the browser.
- [@open-wc/semantic-dom-diff](https://open-wc.org/testing/semantic-dom-diff.html), which allows differing chunks of DOM or HTML for semantic equality.
- [@open-wc/chai-axe-a11y](https://open-wc.org/testing/testing-chai-a11y-axe.html), which provides a Chai plugin to perform automated accessibility tests.

All these powerful tools are configured and available as a single package: [@open-wc/testing](https://open-wc.org/testing/testing.html). You don't need to perform an additional step to start writing your tests!

### Writing Unit Tests

If you're following the Web Components with TypeScript series in the blog, you'll find a [fully functional project](https://github.com/luixaviles/litelement-website) with Web Components implemented using LitElement and TypeScript.

#### Testing the About Page

This would be an example of a basic Web Component implementation:

```
import { LitElement, html, customElement } from 'lit-element';

@customElement('lit-about')
export class About extends LitElement {
  render() {
    return html`
      <h2>About Me</h2>
      <p>
        Suspendisse mollis lobortis lacus, et venenatis nibh sagittis ac.
        ...
      </p>
    `;
  }
}
```

Let's create a file, `/test/about.test.ts`, for its unit tests with the initial content:

```
import { LitElement, html, customElement, css, property } from 'lit-element';
import { About } from '../src/about/about';

describe('About Component', () => {
  let element: About;

  beforeEach(async () => {
    element = await fixture<About>(html` <lit-about></lit-about> `);
  });
});
```

Here's the sequence of actions for clarity:

- `describe('About Component')` gives a meaningful description of the testing scenario.
- `let element: About` declares a variable to _contain_ a reference to the rendered Web Component. This variable will be available for the entire _describe_ block.
- `beforeEach()` function runs as a set of preconditions before running the tests. In this case, it's rendering the string provided as `<lit-about></lit-about>` and puts it in the DOM. This is an asynchronous operation, and will return a `Promise<About>`
- Finally, since we're using `async/await` here, the variable `element` will contain the expected `About` component.

At this point, we're ready to start with the component testing. Add the following test right after `beforeEach`:

```
  it('is defined', () => {
    assert.instanceOf(element, About);
  });
```

This test _asserts_ that the `element` variable contains an instance of the `About` class.

Next, add the following _it_ block to test the title rendering:

```
  it('renders \'About Me\' as a title', () => {
    const h2 = element.shadowRoot!.querySelector('h2')!;
    expect(h2).to.exist;
    expect(h2.textContent).to.equal('About Me');
  });
```

Here's the sequence of actions explained:

- `it()` function sets a test with a meaningful description for its purpose.
- `element.shadowRoot!.querySelector('h2')!` looks for a DOM node that matches the specified selector "h2". That element should be part of the _Shadow DOM_. Then, it's required to do it through `element.shadowRoot` call. 
- The previous call can produce `null` as a result (in case there's no match with the selector). In TypeScript, we can use the [non-null assertion operator](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-0.html#non-null-assertion-operator)(`!`). In other words, you're telling the TypeScript compiler: "I'm pretty sure the element exist".
- `expect(h2).to.exist` is the formal _assertion_ to verify that `h2` variable is not `null` or `undefined`.
- Next, there is an _assertion_ about the text content for the provided selector `h2`.

You can apply the same principles to validate the rendered paragraph:

```
  it('renders a paragraph', () => {
    const paragraph = element.shadowRoot!.querySelector('p')!;
    expect(paragraph).to.exist;
    expect(paragraph.textContent).to.contain(
      'Suspendisse mollis lobortis lacus'
    );
  });
```

#### Testing the Blog Card Component

As you may remember, the Blog Card Component implementation is using LitElement too:

```
import { LitElement, html, customElement, css, property } from 'lit-element';
import { Post } from './post';

@customElement('blog-card')
export class BlogCard extends LitElement {
  static styles = css`
    // your styles goes here
  `;

  @property({ type: Object }) post?: Post;

  render() {
    return html`
      <div class="blog-card">
        <div class="blog-description">
          <h1>${this.post?.title}</h1>
          <h2>${this.post?.author}</h2>
          <p>${this.post?.description}</p>
          <p class="blog-footer">
            <a class="blog-link" @click="${this.handleClick}">Read More</a>
          </p>
        </div>
      </div>
    `;
  }

  public handleClick() {
    this.dispatchEvent(
      new CustomEvent('readMore', { detail: this.post })
    );
  }
}
```

Let’s create another file for the unit tests as `/test/blog-card.test.ts` with the initial content:

```
import { assert, expect, fixture, html, oneEvent } from '@open-wc/testing';
import { BlogCard } from '../src/blog/blog-card';
import { Post } from '../src/blog/post';

const post: Post = {
  id: 0,
  title: 'Web Components Introduction',
  author: 'Luis Aviles',
  description: 'A brief description of the article...',
};

describe('Blog Card Component', () => {
  let element: BlogCard;

  beforeEach(async () => {
    element = await fixture<BlogCard>(html`<blog-card .post="${post}"></blog-card> `);
  });
});
```

There's a lot in common with the previous test file. However, in this case, it's needed to set a `Post` object to be sent as a parameter to create the `<blog-card>` component.

Let's add some tests right after `beforeEach` function:

```
  it('is defined', () => {
    assert.instanceOf(element, BlogCard);
  });

  it('defines a post attribute', () => {
    expect(element.post).to.equal(post);
  });
```

The first test is about an instance assertion: _Is this 'element' an instance of BlogCard class?_.

The second test goes a little bit deeper since it's verifying the value of the custom element property `post`. Remember that property was defined using the `@property` decorator.

Now, let's add some tests to verify the rendering of the different sections from our Web component:

```
  it('renders \'Web Components Introduction\' as a title', () => {
    const h1 = element.shadowRoot?.querySelector('h1')!;
    expect(h1).to.exist;
    expect(h1.textContent).to.equal('Web Components Introduction');
  });

  it('renders \'Luis Aviles\' as author', () => {
    const h2 = element.shadowRoot?.querySelector('h2')!;
    expect(h2).to.exist;
    expect(h2.textContent).to.equal('Luis Aviles');
  });

  it('renders a \'Read More\' link', () => {
    const a = element.shadowRoot?.querySelector('a')!;
    expect(a).to.exist;
    expect(a.getAttribute('class')).to.equal('blog-link');
    expect(a.textContent).to.equal('Read More');
  });
```

There's something new here: `expect(a.getAttribute('class'))`. The `getAttribute` function will inspect the DOM and it will look for the `class` attribute. On the template side, it defines the link to _Read More_ about the current blog post: `<a class="blog-link">Read More</a>`

Since `<blog-card>` is a reusable component, it would be useful to test the event propagation once it's selected:

```
  it('dispatch a click event', async () => {
    setTimeout(() => element.handleClick());
    const { detail } = await oneEvent(element, 'readMore') as CustomEvent<Post>;
    expect(detail).to.be.equal(post);
  });
```

- `setTimeout()` function schedules a click action over the web component.
- Next, the `oneEvent` function helps to handle and resolve the expected event named "readMore". Once it's resolved, we should expect a `CustomEvent<Post>`.
- When a Custom Event is fired, you can expect to have a `detail` attribute with an object. The _Object destructuring_ would be useful in this case.
- Finally, there's an assertion to verify the detail value. It should be the `post` set as an attribute for the Web Component.

### Running the Tests

Just run the following commands:

- `npm run test`, a single running of the whole suite of tests.
- `npm run test:watch`, enable a _watch mode_. Especially useful to see the test results while you're performing changes into the test files.

The output for the first command would be as follows:

[![screenshot-output-testing-karma](https://images.ctfassets.net/zojzzdop0fzx/19NDdB2ooZF7i4ntzjBX9R/4f1493d5b0e90bc222b29d113a31710d/screenshot-output-testing-karma.png)](https://images.ctfassets.net/zojzzdop0fzx/19NDdB2ooZF7i4ntzjBX9R/4f1493d5b0e90bc222b29d113a31710d/screenshot-output-testing-karma.png)

### Source Code Project

Find the complete project and these tests in this GitHub [repository](https://github.com/luixaviles/litelement-website). Do not forget to give it a star ⭐️ and play around with the code.

You can follow me on [Twitter](https://twitter.com/luixaviles) and [GitHub](https://github.com/luixaviles) to see more of my work.

* * *

_This Dot Labs is a modern web consultancy focused on helping companies realize their digital transformation efforts. For expert architectural guidance, training, or consulting in React, Angular, Vue, Web Components, GraphQL, Node, Bazel, or Polymer, visit [thisdotlabs.com](https://www.thisdotlabs.com)._

_This Dot Media is focused on creating an inclusive and educational web for all. We keep you up to date with advancements in the modern web through events, podcasts, and free content. To learn, visit [thisdot.co](https://www.thisdot.co)._