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’sBrowser
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 eventfind()
returns anElementHandle
based on the specifiedSelector
findProperty: ()
returns an element’s property value based on the specifiedSelector
goto()
navigates the browser to the specified URLhasText()
asserts that text exists on the pageselect()
selects a value within anHTMLSelectElement
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:
- Arrange
- Act
- 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’schildren.length
valuetoBeGreaterThan(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 theElementHandle
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