Brian F Love
Learn from a Google Developer Expert focused on Angular, Web Technologies, and Node.js from Portland, OR.

Generate Angular Tests with QA Wolf

Learn how to generate Jest and Puppeteer tests for Angular applications with QA Wolf.

Introducing QA Wolf

I've been playing around with QA Wolf for generating tests for an Angular project, and so far, I've been really impressed. According to their documentation:

QA Wolf is an open source library for creating browser tests and scripts

Getting Started with QA Wolf

First things first, let's install QA Wolf and the appropriate TypeScript definitions:

npm install -D qawolf @types/{node,puppeteer,jest}

Note, TypeScript is optional, but since we are using QA Wolf in the context of Angular, it makes sense to leverage TypeScript in our tests.

Next, we'll use the npx qawolf record command to open our project and record the test using Chromium. Be sure to start your Angular application first using npm start or whatever custom scripts you might be using.

npx qawolf record http://localhost:4200 home

Once Chromium is opened, go through and perform the end-to-end tests as a user would, and then switch back to your terminal and hit Enter to write the test files.

The beauty of QA Wolf is the @qawolf/web library that is injected into the Chromium browser. This library intercepts and records your actions, which are converted to a workflow for your tests.

Finally, we execute the test via:

npx qawolf test

Chromium opens, and our puppeteer and jest tests are executed.

Generated Tests

In this example, I created a new test for the home page, and therefore called the test "home". This resulted in a new .qawolf/tests/home.test.js file:

const { launch } = require('qawolf');
const selectors = require('../selectors/home');

describe('home', () => {
  let browser;

  beforeAll(async () => {
    browser = await launch({ url: 'http://localhost:4200/' });
  });

  afterAll(() => browser.close());

  it('can click "menu" button', async () => {
    await browser.click(selectors[0]);
  });

  it('can click "mat-input-0" input', async () => {
    await browser.click(selectors[1]);
  });

  it('can type into "mat-input-0" input', async () => {
    await browser.type(selectors[2], 'angular');
  });

  it('can Enter', async () => {
    await browser.type(selectors[3], '↓Enter↑Enter');
  });

  it('can click "Unsubscribe from RxJS Observables to avo..." p', async () => {
    await browser.click(selectors[4]);
  });
});

Let's quickly review the code that was generated by QA Wolf:

  • First, we use the launch() asynchronous function to open the Chromium browser to the specified URL.
  • Then, we have a series of tests that leverage the Browser API that extends Puppeteer's Browser API to perform actions in the browser instance.
  • Finally, after all of the tests have executed, we invoke the close() method to close the Chromium browser.

Here is a quick rundown of the available methods on the Browser instance:

  • click() triggers a click event
  • find() returns an ElementHandle based on the specified Selector
  • findProperty: () returns an element's property value based on the specified Selector
  • goto() navigates the browser to the specified URL
  • hasText() asserts that text exists on the page
  • select() selects a value within an HTMLSelectElement
  • type() finds an element, focuses on the element, and then types the specified value

I would also encourage you to check out the official documentation that has a section on reviewing the generated test code.

In a general sense, the test follows the prescribed "AAA" pattern:

  1. Arrange
  2. Act
  3. Assert

The test arranges the browser instance and launches the specified URL. The test acts by interacting with the browser via the click(), type() and click() methods.

However, you'll notice that, for the most part, the final step of asserting is missing from our tests.

Selectors

QA Wolff has a built-in strategy for generating selectors. While this is well architected, it is a best practice to use data- bindings in our application's source code to ensure the longetivity of our tests.

describe('home', () => {
  // code omitted for brevity

  it('can click "menu" button', async () => {
    await browser.click({ css: "[data-qa='mat-toolbar-menu-btn']" });
  });

  it('can click "mat-input-0" input', async () => {
    await browser.click({ css: "[data-qa='menu-dialog-search-input']" });
  });

  it('can type into "mat-input-0" input', async () => {
    await browser.type(
      { css: "[data-qa='menu-dialog-search-input']" },
      'angular'
    );
  });

  it('can Enter', async () => {
    await browser.type(
      { css: "[data-qa='menu-dialog-search-input']" },
      '↓Enter↑Enter'
    );
  });
});

In the example above I've removed the use of the imported selectors array and have replaced them with an object literal that has a single css property. Using this property we can specify a CSS selector to locate the element in the DOM.

There are additional properties that you can use to locate an element, such as html or text.

Adding Assertions

As I mentioned previously, we need to add assertions to our test to conclude the Arrage, Act, and Assert fundamental steps of testing. This is where we leverage both Puppeteer and Jest.

We want to assert that the search had results. We can accomplish this by asserting the length of the children of the container element is greater than 0. To do that, we need to use the find() method to access the ElementHandle for the results container. Let's take a look at the test:

describe('home', () => {
  // code omitted for brevity

  it('has search results', async () => {
    await browser.type(
      { css: "[data-qa='menu-dialog-search-input']" },
      '↓Enter↑Enter'
    );
    const hits = await browser.find({
      css: "[data-qa='search-hits'] .ais-Hits"
    });
    expect(await hits.evaluate(node => node.children.length)).toBeGreaterThan(
      0
    );
  });
});

Let's break this down:

  • First, we use the type() method to type the down and up keystrokes for the "Enter" key.
  • Then, we use the find() method to access the <div class="ais-Hits"></div> element. In my application I am using Algolia's instant search feature for Angular, which wraps the search results with this element. I should note that the test is a little brittle now, as I am targetting an element in the DOM that is generated by the Algolia library.
  • Finally, we expect() that the node's children.length value toBeGreaterThan(0)

When learning QA Wolf, I did discover that you will need to also learn some of the Puppeteer and Jest APIs as well. In the example above, I had to learn how the ElementHandle API works in Puppeteer.

Snapshot Testing

Jest provides snapshot testing that we can use to verify that our UI has not changed since the last snapshot was taken:

describe('search', () => {
  // code omitted for brevity

  it('matches the snapshot', async () => {
    const hits = await browser.find({ css: "[data-qa='search-hits']" });
    expect(hits.jsonValue()).toMatchSnapshot();
  });
});

In the code above we:

  • Use the browser API's find() method to get a reference to the ElementHandle for our search results.
  • Use the element handle API jsonValue() method to get the JSON representation of the object.
  • Assert that the JSON object matches our snapshot using Jest's toMatchSnapshot() method.

Conclusion

In conclusion, we have learned that:

  • QA Wolf is easy to get started with
  • QA Wolf scaffolds and generates the code that is (mostly) necessary for the arrange and act steps
  • We need to have some basic understanding of using both Puppeteer and Jest to write our assertions

Brian F Love

Hi, I'm Brian. I am interested in TypeScript, Angular and Node.js. I'm married to my best friend Bonnie, I live in Portland and I ski (a lot).