DEV Community

This Dot Media
This Dot Media

Posted on • Originally published at labs.thisdot.co on

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

|- lit-element-website/
    |- cypress/
        |- fixtures/
        |- integrations/
        |- plugins/
        |- support/
Enter fullscreen mode Exit fullscreen mode

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"]
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode
  • 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

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

module.exports = on => {
  on('file:preprocessor', cypressTypeScriptPreprocessor);
};
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Adding npm scripts

Add the following scripts into package.json file:

{
  "scripts": {
    "cypress:run": "cypress run --headless --browser chrome",
    "cypress:open": "cypress open"
  }
}
Enter fullscreen mode Exit fullscreen mode
  • 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');
  });
});
Enter fullscreen mode Exit fullscreen mode

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');
      });
  });
Enter fullscreen mode Exit fullscreen mode

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);
  });
});
Enter fullscreen mode Exit fullscreen mode

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);
      });
  });
});
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

npm run cypress:open
Enter fullscreen mode Exit fullscreen mode

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.

Discussion (0)