Picture of Brian Love wearing black against a dark wall in Portland, OR.

Brian Love

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:

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

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:

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:

Conclusion

In conclusion, we have learned that: