Pitfalls to Avoid in Playwright for Node.js
Tags: node, playwright
Published: 2023-08-23 14:06:28

Automation testing has become a fundamental part of web development, and Playwright has emerged as one of the most powerful end-to-end testing tools. Thanks to its robust API and multi-browser support, it's easy to test sites and web apps.

At the same time, Playwright can present some challenges if not approached correctly. Several pitfalls can compromise the effectiveness and performance of your tests, potentially leading to inaccurate results.

In this article, we'll cover the seven most common mistakes developers should avoid when dealing with Playwright, to ensure that your testing process is quality-oriented, efficient, and reliable.

It's time to become a Playwright ninja!

## 7 Common Mistakes to Avoid in Playwright for Node.js

Let's take a look at the major pitfalls you should avoid when using Playwright for browser automation.

_If you want a quick overview of Playwright before diving in, check out our post [An Introduction to Playwright for Node.js](https://blog.appsignal.com/2023/07/12/an-introduction-to-playwright-for-nodejs.html)._

### 1. Configuring Playwright for Node.js Poorly

A wrong Playwright configuration can lead to failures, false positives, and unreliable results. To avoid that, you must set up the tool correctly. Playwright supports several [configuration options](https://playwright.dev/docs/test-configuration), and you should particularly pay attention to:

- **Environment variables:** Tests can access env variables during runtime, allowing you to avoid hard-coding strings and parameters in your scripts. Environment variables are a good way to define the required configurations for your tests. For example, you could use them to specify the browser type, page timeout, viewport size, and more. Always add some fallback values to avoid failures if the envs aren't defined.
- **CLI parameters**: Playwright offers several CLI (Command-Line Interface) options to configure how tests are run. Familiarize yourself with all the [CLI options](https://playwright.dev/docs/test-cli) available and learn their effects. For example, the `--headed` or `--headless` flags control whether the browser should be launched with UI or headless mode, respectively.
- `playwright.config.ts`: This is the main configuration file where you can define settings for all tests. For example, here you can enable cross-browser testing and force Playwright to run tests on the specified browser. That's essential to ensure cross-browser compatibility and see how your script behaves on Chromium, Firefox, and/or WebKit.

### 2. Writing Short Tests For No Real Reason

Separating each assertion into an individual test to keep tests short is a mistake you shouldn't make. This practice brings no significant benefit and only slows down test execution. Short and focused tests are great for unit tests, but in end-to-end testing scenarios, it's more practical to have long tests that reproduce the entire user journey.

The goal is to cover the complete application flow. It doesn't matter if tests end up being long and involve multiple assertions. When a user interacts with your app, they perform several actions.

Also keep in mind that if a test fails, Playwright will provide you with specific messages to help you understand what went wrong and in what part of the script. As long as they are easy to read and maintain, you don't have to worry about writing long scripts.

### 3. Testing Third-Party Applications

When you test an application flow, you may be tempted to test integrations with third-party services (e.g., a headless CMS), but that's a trap!

As explained in the [Playwright official documentation](https://playwright.dev/docs/best-practices#avoid-testing-third-party-dependencies), you should avoid testing third-party applications and dependencies. These aren't under your control, and their behavior or UI may change without notice. Dealing with them can lead to false negatives, and you don't want that.

Instead, you should focus only on testing elements and features within your application. If you need to interact with an external service, take advantage of the [Playwright Network API](https://playwright.dev/docs/network) to mock API responses, as in the example below:

```javascript
await page.route("https://api.your-cms.com/articles", (route) => {
  route.fulfill({
    status: 200,
    contentType: "application/json",
    body: JSON.stringify({
      articles: [
        {
          title: "An Introduction to Async Stack Traces in Node.js",
          description:
            "Let's dive into how async stack traces work and how they can be used to debug code.",
          url: "https://blog.appsignal.com/2023/05/17/an-introduction-to-async-stack-traces-in-nodejs.html",
        },
        // omitted for brevity...
      ],
    }),
  });
});
```

With the [`page.route()`](https://playwright.dev/docs/api/class-page#page-route) function, you can simulate the behavior of third-party services. When the web page makes a request to `https://api.your-cms.com/articles`, Playwright intercepts it and responds with the mocked JSON response. This makes your tests more isolated and reliable, allowing them to succeed even when third-party dependencies aren't available.

### 4. Not Using Proper Selectors

Selectors play a key role in Playwright tests as they give you the ability to identify and interact with HTML elements.
Failing to use appropriate selectors can lead to flaky results, increased maintenance effort, and decreased test stability.

One of the most common mistakes to make is using poor selectors that are overly generic. The web page under test might have a single `<span>` element, but selecting it with `page.locator('span')` is likely to lead to unintended actions on an incorrect element in the future. That's because `<span>` is a pretty generic tag!

On the other hand, overly specific selectors can tightly couple tests to a page structure. In this scenario, a minor change in the HTML of the target page may break your tests. This can result in frequent test failures and require consistent maintenance. Also, long and complex selectors reduce code readability.

Let's consider an example. Suppose you want to retrieve the following button in a form:

```html
<button class="form-button login-button" type="submit">Login</button>
```

This selector would be too generic:

```javascript
page.locator("button");
```

This one too specific:

```javascript
page.locator("form > button.form-button.login-button");
```

While this one is resilient to DOM changes and restrictive enough to select the desired element:

```javascript
page.getByRole("form > button", { type: "submit" });
```

Devising the right selectors is an art. However, remember that [Playwright can generate test code](https://playwright.dev/docs/codegen) as you take actions in the browser. Thanks to this advanced feature, the tool will analyze the DOM of your target page and figure out the best selectors for you.

### 5. Ignoring Playwright's Debugging Capabilities

Don't forget that a test can fail not only because of issues with the functionality being tested, but also because of a bug in the test code itself. Here's why it's so important to debug your scripts. Playwright comes to the rescue with several advanced, powerful, built-in [debugging tools](https://playwright.dev/docs/debug). These include:

- **Debug mode**: Running a test with the `--debug` option opens the [Playwright Inspector](https://playwright.dev/docs/debug#playwright-inspector). This GUI tool allows you to step through a running script, edit locators in real time, and see actionability logs. This is a built-in alternative to the recommended way to debug tests in Playwright (using your IDE's debugger). If you are using Visual Studio Code, take a look at the [Playwright Test for VSCode extension](https://marketplace.visualstudio.com/items?itemName=ms-playwright.playwright).
- **Logs**: If you use `console.log()` directly in your script, it won't work. To leverage that function, you need to wrap it with the [`page.evaluate()`](https://playwright.dev/docs/api/class-page#page-evaluate) method, as below:

```javascript
await page.evaluate(() => {
  console.log("Page title:", document.title);
});
```

Printing values in the console will help you identify bugs and understand how your code is behaving.

- **Breakpoints**: You can define breakpoints in your tests with the [`page.pause()`](https://playwright.dev/docs/api/class-page#page-pause) function. When encountering this instruction, Playwright will stop executing the script and wait for the user to click the “Resume” button in the page overlay, or call `playwright.resume()` in the DevTools console.

### 6. Failing to Handle Errors

Your tests can throw errors and exceptions. Ignoring them can lead to false positives or negatives, undermining the effectiveness of the entire testing process. To avoid that issue, it's crucial to handle exceptions and errors gracefully.

In Playwright, you can implement error handling with the `try-catch` statement:

```javascript
test("example test", async () => {
  // test logic...

  try {
    // specific test operation
  } catch (error) {
    // handle error here
  }

  // test continues...
});
```

In this example, the `try` block contains test code that could potentially throw an error. That will be caught by the `catch` block, which might log some data, retry the operation, or take another action depending on the specific situation.

Thus, it's important to distinguish between issues in your Node.js application and errors in tests. Implementing proper error handling will help you identify and isolate the latter.

### 7. Relying on Hard Waits

Hard waits, also known as static or fixed waits, involve pausing test execution for a specific amount of time.
Playwright supports hard waits through the [`page.waitForTimeout()`](https://playwright.dev/docs/api/class-page#page-wait-for-timeout) function:

```javascript
// wait for 2 second
page.waitForTimeout(2000);
```

The official docs mark this feature as 'discouraged', clearly stating that you should never rely on it in production. The reason is that, in most cases, it's virtually impossible to know the right timeout to keep on hold. The same test may be successful once and then fail because of a network slowdown. In addition, hard waits introduce unnecessary delays. That's especially true if the pending operation completes way before the specified duration.

For this reason, Playwright also provides built-in smart wait functions. These allow you to wait for specific events to occur before proceeding with test execution. Some of the most popular ones are:

- **`page.waitForSelector()`**: Waits until an element matching the specified selector is present in the DOM.
- **`page.waitForNavigation()`**: Waits for a navigation event to occur, such as clicking a link or submitting a form, before continuing with the test.
- **`page.waitForFunction()`**: Waits until the provided JavaScript predicate function returns a truthy value.

These functions are designed to wait for dynamic elements, page navigation, network requests, and other asynchronous operations. Thanks to them, it's possible to guarantee a test's progress as soon as the desired conditions are met.

## Wrapping Up: Don't Let Common Playwright Mistakes Slow You Down!

In this blog post, we explored the most common mistakes you can make in Playwright, their effects on your tests, and how to avoid them.

You now know:

- How important it is to configure Playwright and use all of its features
- What elements to exclude from your testing logic
- What functions and methods to avoid

Thanks for reading!

**P.S. If you liked this post, [subscribe to our JavaScript Sorcery list](https://blog.appsignal.com/javascript-sorcery) for a monthly deep dive into more magical JavaScript tips and tricks.**

**P.P.S. If you need an APM for your Node.js app, go and [check out the AppSignal APM for Node.js](https://www.appsignal.com/nodejs).**