Testing Web Components with Cypress and TypeScript
Tags: litelementtypescript
Published: 2020-11-06 14:39:12

---
title: Testing Web Components with Cypress and TypeScript
published: true
date: 2020-10-28 17:00:49 UTC
tags: litelementtypescript
canonical_url: https://www.thisdot.co/blog/testing-web-components-with-cypress-and-typescript
---

In my previous posts, I covered a wide range of topics about LitElement and TypeScript. Then I created a [Single Page Application](https://github.com/luixaviles/litelement-website) based on Web Components.

Let's dive deep into this project to understand how to add the End-to-End(E2E) testing capability using [Cypress](https://www.cypress.io/) and TypeScript.

### The Shadow DOM

The Shadow DOM API is about web components encapsulation, meaning it keeps the styling, markup, and behavior hidden and separate from other code on the page. This API provides a way to attach a separated DOM to an element.

[![shadow-dom](https://images.ctfassets.net/zojzzdop0fzx/5ILbAaO2HgwsuMebp4shCb/21efc5a8c8b37fbac78b98c02b9bbc82/shadow-dom.png)](https://images.ctfassets.net/zojzzdop0fzx/5ILbAaO2HgwsuMebp4shCb/21efc5a8c8b37fbac78b98c02b9bbc82/shadow-dom.png)

To clarify, let's consider the `/about` page from our application:

[![shadow-root-screenshot](https://images.ctfassets.net/zojzzdop0fzx/5ZPlQX8ZHA5VWV0HasPlzp/c2c640db8619625db50ea4fcf5f01399/shadow-root-screenshot.png)](https://images.ctfassets.net/zojzzdop0fzx/5ZPlQX8ZHA5VWV0HasPlzp/c2c640db8619625db50ea4fcf5f01399/shadow-root-screenshot.png)

As you can see, The Shadow DOM allows hidden DOM trees and starts with a _shadow root_: `#shadow-root (open)`. The word `open` means you can access the shadow DOM through JavaScript.

However, it's feasible to attach a shadow root with a `closed` mode, meaning you won't be able to access the shadow DOM. It's a really good way to encapsulate an inner structure or implementation.

### Adding Cypress Support

#### Installing Cypress

Install Cypress as part of the development dependencies:

```
npm install --save-dev cypress
```

This will produce the following output:

```
litelement-website$ npm install --save-dev cypress

Installing Cypress (version: 5.3.0)

  ✔ Downloaded Cypress
  ✔ Unzipped Cypress
  ✔ Finished Installation /Users/luixaviles/Library/Caches/Cypress/5.3.0

You can now open Cypress by running: node_modules/.bin/cypress open

https://on.cypress.io/installing-cypress
```

After runing the Cypress installer, you'll find the following folder structure:

```
|- lit-element-website/
    |- cypress/
        |- fixtures/
        |- integrations/
        |- plugins/
        |- support/
```

By default, you'll have several JavaScript files in those new folders, including examples and configurations. You may decide to keep those examples for Cypress reference or remove them from the project.

#### Adding The TypeScript Configuration

Since we have a TypeScript-based project, we can consider the new `cypress` directory as the root of a new sub-project for End-to-End testing.

Let's create the `/cypress/tsconfig.json` file as follows:

```
{
  "compilerOptions": {
    "strict": true,
    "baseUrl": "../node_modules",
    "target": "es6",
    "lib": ["es5", "es6", "dom", "dom.iterable"],
    "types": ["cypress"]
  },
  "include": ["**/*.ts"]
}
```

This file will enable the `cypress` directory as a TypeScript project and we'll be ready to set more properties and configurations if that is needed in the future.

#### Cypress Configurations

Next, we'll update the autogenerated `cypress.json` file as follows:

```
{
  "supportFile": "cypress/support/index.ts",
  "experimentalShadowDomSupport": true
}
```

- The `supportFile` parameter will set up a path to the file to be loaded before loading test files.
- The `experimentalShadowDomSupport` flag was needed to enable shadow DOM testing in previous versions. However, since `v5.2.0` it [is no longer necessary](https://docs.cypress.io/guides/references/changelog.html#5-2-0).

#### Webpack Configuration and TypeScript Compilation

First, install the following tools:

```
npm install --save-dev webpack typescript ts-loader
```

Since we'll use TypeScript for our Cypress tests, we'll be using `Webpack` and `ts-loader` to compile and process these files.

#### The Cypress Webpack preprocessor

Install the [cypress-webpack-preprocessor](https://github.com/cypress-io/cypress-webpack-preprocessor):

```
npm install --save-dev @cypress/webpack-preprocessor
```

Update the content of `plugins/index.js` file to:

```
const cypressTypeScriptPreprocessor = require('./cy-ts-preprocessor');

module.exports = on => {
  on('file:preprocessor', cypressTypeScriptPreprocessor);
};
```

On other hand, create a JavaScript file: `cypress/plugins/cy-ts-preprocessor.js` with the `Webpack` configuration:

```
const wp = require('@cypress/webpack-preprocessor');

const webpackOptions = {
  resolve: {
    extensions: ['.ts', '.js'],
  },
  module: {
    rules: [
      {
        test: /\.ts$/,
        exclude: [/node_modules/],
        use: [
          {
            loader: 'ts-loader',
          },
        ],
      },
    ],
  },
};

const options = {
  webpackOptions,
};

module.exports = wp(options);
```

At this point, make sure the file structure is as shown below:

```
|- lit-element-website/
    |- cypress/
        |- fixtures/
        |- integrations/
        |- plugins/
            |- index.js
            |- cy-ts-preprocessor.js
        |- support/
            |- commands.ts
            |- index.ts
        |- tsconfig.json
    |- cypress.json
```

#### Adding npm scripts

Add the following scripts into `package.json` file:

```
{
  "scripts": {
    "cypress:run": "cypress run --headless --browser chrome",
    "cypress:open": "cypress open"
  }
}
```

- `cypress:run` defines a script to run all End-to-End tests in a _headless_ mode in the command line. That means the browser will be hidden.
- `cypress:open` Will fore Electron to be shown. You can use `cypress run --headed` as another option with the same effect.

You can see all available parameters to run commands on Cypress [here](https://docs.cypress.io/guides/guides/command-line.html#cypress-run).

### Adding the Tests

You should be ready to add your test files at this point.

#### Testing the About Page

Let's create our first test file into `cypress/integration` folder as `about.spec.ts`:

```
// about.spec.ts
describe('About Page', () => {
  beforeEach(() => {
    const baseUrl = 'http://localhost:8000/about';
    cy.visit(baseUrl);
  });

  it("should show 'LitElement Website' as title", () => {
    cy.title().should('eq', 'LitElement Website');
  });

  it("should read 'About Me' inside custom element", () => {
    cy.get('lit-about')
      .shadow()
      .find('h2')
      .should('contain.text', 'About Me');
  });
});
```

The previous file defines two test cases. The second one expects to have access to the Shadow DOM through `.shadow()` function, which yields the new DOM element(s) it found. Read more about this syntax [here](https://docs.cypress.io/api/commands/shadow.html#Syntax).

[![shadow-root-screenshot](https://images.ctfassets.net/zojzzdop0fzx/1AYN1brsOvw7VABKlcRGnJ/b1dc55859e78d8e840795204f1ad3622/shadow-root-screenshot.png)](https://images.ctfassets.net/zojzzdop0fzx/1AYN1brsOvw7VABKlcRGnJ/b1dc55859e78d8e840795204f1ad3622/shadow-root-screenshot.png)

As this screenshot shows, the `h2` element contains the title of the About page and we can use `contain.text` to complete the assertion.

In case you want to take more control of the DOM content of your web component, you can try something like this instead:

```
it("should read 'About Me' inside custom element ", () => {
    cy.get('lit-about')
      .shadow()
      .find('h2')
      .should(e => {
        const [h2] = e.get();
        // Here we have the control of DOM conten from custom element
        console.log('h2!', h2, h2.textContent);
        expect(h2.textContent).to.contains('About Me');
      });
  });
```

#### Testing the Blog Posts Page

Let's create a new TypeScript file inside `cypress/integration` folder as `blog-posts.spec.ts`:

```
// blog-posts.spec.ts

describe('Blog Posts Page', () => {
  beforeEach(() => {
    const baseUrl = 'http://localhost:8000/blog';
    cy.visit(baseUrl);
  });

  it("should read 'Blog Posts' as title", () => {
    cy.get('lit-blog-posts')
      .shadow()
      .find('h2')
      .should('contain.text', 'Blog Posts');
  });

  it("should display a list of blog cards", () => {
    cy.get('lit-blog-posts')
      .shadow()
      .find('blog-card')
      .its('length')
      .should('be.gt', 0);
  });
});
```

In this case, we expect to have some `blog-card` elements inside the Blog Posts page.

#### Testing the Blog Card Elements

Let's create a new TypeScript file inside `cypress/integration` folder as `blog-card.spec.ts`:

```
// blog-card.spec.ts

describe('Blog Card', () => {
  const titles = [
    'Web Components Introduction',
    'LitElement with TypeScript',
    'Navigation and Routing with Web Components',
  ];

  const author = 'Luis Aviles';

  beforeEach(() => {
    const baseUrl = 'http://localhost:8000/blog';
    cy.visit(baseUrl);
  });

  it('should display a title', () => {
    cy.get('lit-blog-posts')
      .shadow()
      .find('blog-card')
      .each((item, i) => {
        cy.wrap(item)
        .shadow()
        .find('h1')
        .should('contain.text', titles[i]);
      });
  });

  it('should display the author\'s name', () => {
    cy.get('lit-blog-posts')
      .shadow()
      .find('blog-card')
      .each((item, i) => {
        cy.wrap(item)
        .shadow()
        .find('h2')
        .should('contain.text', author);
      });
  });
});
```

The `Blog Card` scenario defines the title values for every blog post and the author's name for all of them.

- The `should display a title` test case iterates through all `blog-card` elements and access(again) to the Shadow DOM through `.shadow()` function to compare the title value.
- The `should display the author's name` applies the same logic as above to verify the author's name.

See more details in the following screenshot:

[![spec-blog-card](https://images.ctfassets.net/zojzzdop0fzx/2trGAAyvE3X41f7BuhuIbA/212e9119d893befdff7a5aaecb0de7ef/spec-blog-card.png)](https://images.ctfassets.net/zojzzdop0fzx/2trGAAyvE3X41f7BuhuIbA/212e9119d893befdff7a5aaecb0de7ef/spec-blog-card.png)

### Running the Tests

Some tests have been implemented already. Let's run the scripts we defined before as follows:

```
npm run cypress:run
```

As stated before, the previous script will run all End-to-End(E2E) tests entirely on the command line.

```
npm run cypress:open
```

This command will open the Cypress Test Runner through the Electron window.

### Source Code Project

Find the complete project in this GitHub repository: [https://github.com/luixaviles/litelement-website](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 about my work.