logo

Fast, comprehensive test coverage for your Web Components

I've been relying on Web Components to build UI widgets for the last few years. You need a fast way to run tests against your component in isolation as well as end to end tests once it is integrated into your application.

Isolated component testing

The isolated component tests are a place you can strive for comprehensive coverage and try to cover the available featureset. The Open WC effort provides an excellent guide for component specific testing. Here is a snippet of a test against a standalone search widget that is eventually integrated into a larger site.

import { fixture, expect } from '@open-wc/testing';
import '../index.js';    

describe('', function() {
  it('renders search box', async () => {
    const el = await fixture('');
    expect(el.innerHTML.indexOf('input') > -1).to.equal(true);
  });

  it('search displays results', async function() {
    const el = await fixture('');    
    let searchBox = el.querySelector('input[name="query"]');
    searchBox.value = 'play';
    document.getElementById('submit-search').click();
    let results = await elementExists('.results-list');
    expect(el.querySelectorAll('.results-list li').length).to.be.above(9);
  })
})

Integrated full application testing

End to end functional testing run against a full application that includes several components will provide massive peace of mind. I am using puppeteer based tests to run through the common application flows that require multiple component interaction now. I am able to cover basic flows and do things like make sure all by analytics events are firing in a smaller suite than can run quickly before code is merged. My end to end test suite doesn't attempt to cover every edge case of interacting with a specific component, that is left to the isolated component tests but whenever I upgrade the version of a component inside the full web app the end to end tests run to ensuring user flows requiring multiple components work. Here are snippets showing the puppeteer bootstrap file and snippets from a test:

const puppeteer = require('puppeteer')
const { expect } = require('chai')
const _ = require('lodash')
const chalk = require('chalk')

const globals = _.pick(global, ['browser', 'expect', 'chalk'])

const headfulness = { // eslint-disable-line no-unused-vars
  headless: false,
  devtools: true,
  defaultViewport: { width: 900, height: 800 },
  slowMo: 30, // milliseconds
}

const opts = {
  args: [ '--start-maximized', '--window-size=1920,1040']
}

before(async () => {
  global.expect = expect
  global.browser = await puppeteer.launch(opts)
  global.chalk = chalk
  global.testData = {}
})

after(async () => {
  browser.close() // eslint-disable-line no-undef
  global.expect = globals.expect
  global.browser = globals.browser
  global.chalk = chalk
  global.testData = {}
})

describe('Load "/" route, fill form there, and submit it', async function() {
  let page
  const urlBase = 'http://localhost:1337/'

  before(async function() {
    page = await browser.newPage()
  })

  describe('Navigate to /', function() {
    before(async function() {
      await page.goto(`${urlBase}`)
    })

    it('should see a seachbox, enter text', async function() {
      await page.waitFor('input[name="coprocure_query"]');
      let searchString = await page.evaluate(() => {
        let searchBoxSelector = '.middle-section input[name="coprocure_query"]';
        document.querySelector(searchBoxSelector).value = "play";
        return document.querySelector(searchBoxSelector).value;
      });
      expect(searchString).to.eql('play')
    })

    it('should submit form, see search results', async function() {
      const searchBtn = await page.$('.middle-section .search-now')
      await searchBtn.click()
      await page.waitForSelector('.search-results')
      let searchResultLength = await page.evaluate(() => {
        return document.querySelectorAll('.search-results li').length;
      });
      expect(searchResultLength).to.eql(10)
    })

    it('be looking at 1-10 results on first page', async function() {
      let countText = await page.evaluate(() => {
        return document.querySelector('.result-count').textContent.indexOf('Showing 1-10 of');
      });
      expect(countText).to.eql(0)
    })
  })

  after(async function() {
    await page.close()
  })
})

Additional concerns

I am using these methods successfully to provide reliability focusing on test suites that can run quickly. Test systems that cause too much additional overhead risk being skipped by dev teams. I've tried other CI approaches with web applications in the past. We used Cypress.io extensively at ZapLabs and it was helpful. I also liked my setup with zombie and codeship in 2015. Additional testing concerns not addressed here are cross browser evaluation and UI fidelity. We need to make sure all flows function at all screen sizes in all browser engines. Doing automated screenshot comparisons to ensure UI fidelity seems promising if you can run versions against the same dataset to avoid test failures due to data differences.