Skip to content

Testing Web Components with Cypress and TypeScript

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 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 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

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

shadow-root-screenshot

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.

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:

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.

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.

shadow-root-screenshot

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

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. Do not forget to give it a star ⭐️ and play around with the code.

You can follow me on Twitter and GitHub to see more about my work.