4 minute read

Introduction

Puppeteer is a headless Chrome Node.js API to execute actions on a wegpage and assert queries using jest. The tests can be run headless (in the background) or headful (opening a browser window and executing the actions live). Puppeteer provides an API that lets you select elements and interact with the html-template through selectors and events. Those actions are wrapped in a page-object (.po.ts)

Running e2e tests

When you run e2e tests, puppeteers starts a html-server using the built application as it’s entry point. In order to run the e2e tests, the application has to be built. Keep in mind that changing code in files, which change the outcome of the build, will require a rebuild of the application to test the new code. This includes all css, html and mostly all .ts-files (page-objects and test-files are excluded from the build and don’t require a rebuild).

IMPORTANT

  • Do not run the dev-server while running e2e tests. They share the same build-folder and e2e tests are not executable from a dev-server compiled build.

Running all e2e tests:

Note that the build command requires unix tools on path, so on Windows add them to it or use the bash shell

  • npm run build
  • npm run e2e for parallel or npm run e2e:ci for sequential execution

Running individual e2e tests:

  • Pass a pattern through the cli.

Example: npm run e2e -- --testNamePattern fileChooser will only execute tests that include “fileChooser” in the description.

Debugging tests

  • Execute tests headful
  • Execute tests in slow-mo

visualization/jest-puppeteer.config.js

module.exports = {
	launch: {
		headless: true,
		args: ["--allow-file-access-from-files", "--start-maximized"],
		defaultViewport: { width: 1920, height: 1080 }
		// slowMo: 250
	}
}
  • Pipe console.logs from the browser to the terminal (Run enableConsole() from the puppeteer.helper.ts in a test).

Writing e2e tests

Page-Objects

Writing and maintaining e2e-tests can be tedious. Selectors, such as classNames and ids may change and tests need to be adapted. In order to minimize changes across files, we introduce Page-Objects (.po.ts) to wrap our selectors and build our own API to the component.

Rule of thumb: Never use selectors or calls to the page-object in the .e2e.ts-file. Always move them to the page-object.

Example:

public async clickChooser() {
	await expect(page).toClick("file-panel-component md-select", { timeout: 3000 })
}

Instead of working on the page directly, we can call this methods on the fileChooser-page-object in our test. If we change the name of the component, we only have to adapt the page-object instead of all the tests calling this method.

describe("MapTreeViewLevel", () => {
	let mapTreeViewLevel: MapTreeViewLevelPageObject
	let searchPanelModeSelector: SearchPanelModeSelectorPageObject
	let nodeContextMenu: NodeContextMenuPageObject

	beforeEach(async () => {
		// Setting up page-objects
		mapTreeViewLevel = new MapTreeViewLevelPageObject()
		searchPanelModeSelector = new SearchPanelModeSelectorPageObject()
		nodeContextMenu = new NodeContextMenuPageObject()

		// refreshing the page before every test
		await goto()
	})

	describe("Blacklist", () => {
		it("excluding a building should exclude it from the tree-view as well", async () => {
			const filePath = "/root/ParentLeaf/smallLeaf.html"

			// only use page-object-functions to execute actions / events on the webpage
			await searchPanelModeSelector.toggleTreeView()
			await mapTreeViewLevel.openFolder("/root/ParentLeaf")
			await mapTreeViewLevel.openContextMenu(filePath)
			await nodeContextMenu.exclude()

			// use page-object-functions to get some state to verify
			expect(await mapTreeViewLevel.nodeExists(filePath)).toBeFalsy()
		})
	})
})

Setup and teardown

Usually you’d have to setup the server, browser and page yourself. Luckily jest-puppeteer provides global variables, such as page and automatically handles the setup and teardown for us. No need for boilerplate code in our tests.

Stability

Running e2e-tests can lead to timeouts and race conditions depending on how fast your pc is. This can be quite frustrating across multiple developers and a CI. Therefore increasing the stability is very important to prevent the tests from failing randomly.

Common reasons the test is failing

  • Trying to access a selector that is not available (yet)
  • Accessing an old selector and using the data to verify something (reading old classNames and expecting a new className to be set)
  • Fixed delays by calling waitFor(1000) // wait for 1000ms

Best practices

  • Before accessing a selector, wait until it’s available using await page.waitForSelector(MY_SELECTOR)
  • When clicking a button, import the clickButtonOnPageElement function from the ` puppeteer.helper file and use clickButtonOnPageElement(MY_SELECTOR, options) instead of expect(page).toClick(MY_SELECTOR, options)`. This function awaits the selector and clicks on it when it becomes available.
  • After clicking a button or changing the state, use waitForSelector() to verify, that the new state is rendered before continuing

Most used functions

  • page.waitForSelector(MY_SELECTOR) to avoid race conditions
  • page.waitForSelector(MY_SELECTOR, { visible: false }) to check, if a HTMLElement is not visible through css (ng-show)
  • page.waitForSelector(MY_SELECTOR, { hidden: true}) to check, if a HTMLElement is not in the DOM (ng-if)
  • expect(page).toClick() or expect(page).toClick({ button: "right" }) to click on something
  • page.$eval(SELECTOR, el => el[ATTRIBUTE]) to evaluate one HTMLElement and returning the attribute. Example: page.$eval(SELECTOR, el => el.className)
  • page.$$eval(SELECTOR, elements => elements.map(x => x[ATTRIBUTE])) to evaluate multiple HTMLElements and returning the attributes in an array. Mostly used when evaluating a list (like the metrics in the dropdown)