Playwright is an open-source tool developed by Microsoft. It allows you to run tests in various environments. The tests run both with and without a graphical interface.
Playwright itself is written in TypeScript and requires Node.js to run the tests. However, this doesn't mean you’re forced to use TypeScript to write your tests. You can also access the Playwright API via Python, .NET, or Java.
Like Cypress, Playwright is a relatively young project, but it has now established itself as an integral part of the web community.
In this tutorial, we’ll focus on using Playwright with TypeScript to demonstrate its full potential for testing modern web applications. Whether you’re familiar with JavaScript testing frameworks or just getting started, Playwright provides an intuitive setup and a wealth of tools to streamline your testing process.
Installing and Configuring Playwright
Like almost all major testing frameworks, Playwright is available as an npm package. This means you can install it in your application using any JavaScript package manager. In addition to the source code that enables you to run your tests, the Playwright package includes a command-line script that you can use to work with Playwright. It lets you run the tests and can help you create them.
If you install Playwright in your project using the npm install playwright command, you will still need to invest some effort in configuring your test environment. The better option, however, is to use the initializer. This is a feature offered by npm, Yarn, and pnpm. In the case of npm, you run the command npm init playwright@latest within your application. The command installs Playwright and creates the necessary structures in the project so you can start testing straight away.
Playwright provides an intuitive setup and a wealth of tools to streamline your testing process.
Interactive Setup Questions
The setup process is interactive. This means that Playwright asks you a series of questions on the console. The answers have a direct impact on your project.
The first question is regarding where you want to place your E2E tests. The default answer is tests. No matter what name you choose here, Playwright creates the directory for you and also places an initial example test there. Next to this directory, you’ll also find another directory called “tests-examples.” Here you’ll find another test file containing a whole series of example tests that you can use as templates for your own tests.
For the second question, you have to decide whether Playwright should create a GitHub Actions workflow for you. The default here is no. It's obvious that a tool developed by Microsoft would also support Microsoft infrastructure out of the box. However, you don't have to worry if you use a different infrastructure in your project. Fundamentally, Playwright is infrastructure-independent.
The third question allows you to tell Playwright whether you want to install a browser. Playwright can run the tests on one or more browsers. By default, Playwright installs Chromium, Firefox, and WebKit for you. Alternatively, you can install the browsers individually via the command line. If you omit the browser name, Playwright installs all standard browsers. With npx playwright install-deps, you can install the system components required for the browser in addition to the browser.
Running Tests Using Npx and Npm Scripts
Playwright's setup process created the playwright.config.ts file in the root directory of your application. It contains the configuration for your E2E tests. Playwright also created the tests and tests-examples directories for you. The tests' directory contains a file with two simple example tests. The tests-examples directory, on the other hand, contains a more comprehensive E2E test that demonstrates how to write tests using a to-do application as an example.
Fundamentally, Playwright is infrastructure-independent.
With this initial Playwright configuration, you can run all tests with the npx playwright test command. The npx command is part of the Node.js platform and searches the system for the specified script. If it isn't available, npx downloads the package and executes the script. However, once you have installed Playwright in your application, the first case applies. It's even more convenient if you define the E2E command as a npm script in your package.json file. Let's look at an example:
{
"scripts": {
"test:e2e": "playwright test",
"test:e2e:headed": "playwright test --headed",
"test:e2e:chromium": "playwright test --project=chromium",
"test:e2e:firefox": "playwright test --project=firefox",
"test:e2e:webkit": "playwright test --project=webkit",
"test:debug": "playwright test --project=chromium --debug",
"test:ui": "playwright test --ui"
}
}
With this script, you can run your tests with the command npm run test:e2e. While you will save hardly any characters, you won't have to remember a framework-specific command, which can be very helpful when switching between applications with different frameworks. You can also extend the script with additional options without having to explicitly specify them each time you run it.
Writing Your First Test
If you already have experience with JavaScript testing frameworks, getting started with Playwright won't be difficult. You create a new test by calling the test function. This expects a short description of the test case and a callback function containing the actual test as its first argument. You usually implement this callback function as an async function, as Playwright relies heavily on promises, and your tests are much easier to read with async-await.
You can group your tests into test suites with the test.describe method, which in turn accepts a description and a callback function. With the test.beforeAll, test.beforeEach, test.afterEach, and test.afterAll methods, you can define functions that are executed before all tests, before each test, after each test, or after all tests. Typically, you use these setup and teardown routines to prepare your test environment, clean up after a test run, or outsource commonalities across multiple tests to make your tests more compact.
The book management application has a list of records. The first test case is to verify the display of the list. To do this, you can assume that the backend returns three records:
import { expect, test } from "@playwright/test";
test.describe('List', () => {
test('render', async ({ page }) => {
await page.goto('http://localhost:5173/list');
// Check that the heading displays "Book List"
const headline = page.getByRole('heading');
await expect(headline).toHaveText('Book List');
// Check that there are three book titles and verify their names
const titles = page.locator('table > tbody > tr > td:nth-child(2)');
await expect(titles).toHaveCount(3);
await expect(titles).toHaveText([
'Pride and Prejudice',
'To Kill a Mockingbird',
'Moby-Dick'
]);
// Check the publication years for each book
const years = page.getByTestId('year');
await expect(years).toHaveText([
'1813',
'1960',
'1851'
]);
});
});
The example here shows various approaches for locating and inspecting elements on the current page. First, however, you use the goto method of the page object to navigate to the appropriate page in your application. It doesn't matter to Playwright whether it's a classic multi-page application or a single-page application with a modern JavaScript framework. The method returns a Promise object, which you can wait for with await.
If the test run is successful, the Playwright script exits on the console and you receive a short success message. If at least one test fails, Playwright delivers the test report locally via a web server so you can view the results directly in detail. A successful report looks like this:

Preparing the Test Environment With Fixtures
The concept of fixtures follows a similar path, with the goal of keeping your test source code as clean and focused as possible. A requirement of every automated test, whether unit or end-to-end, is that it should be able to run independently of other tests. Therefore, it's crucial that each test encounters a clean environment and leaves the test environment as clean as it can be. This avoids requiring a specific test call chain. If one test in such a chain fails, there's a high probability that all the other tests in the chain will also fail.
To solve this problem, Playwright provides the Fixtures feature, which you can use to prepare your tests.
For the following example, we assume that there is a regular backend for the tests and that it has no data yet:
import { expect, test } from "@playwright/test";
test.describe('List', () => {
test('render', async ({ page }) => {
await page.goto('http://localhost:5173/list');
// Expect the heading to be "Book List" instead of "Bücherliste"
const headline = page.getByRole('heading');
await expect(headline).toHaveText('Book List');
// Verify the titles of the books
const titles = page.locator('table > tbody > tr > td:nth-child(2)');
await expect(titles).toHaveCount(3);
await expect(titles).toHaveText([
'Pride and Prejudice',
'To Kill a Mockingbird',
'Moby-Dick'
]);
// Verify the publication years
const years = page.getByTestId('year');
await expect(years).toHaveText([
'1813',
'1960',
'1851'
]);
});
});
To create a fixture, use the extend method of Playwright's test object. You pass an object to this object defining, for example, the listPage as a method. In the method implementation, you can instantiate your page object model and perform preparatory work, such as creating records. You then integrate the page object model with the use function and can then perform cleanup routines, such as deleting previously created records.
Using a fixture has the advantage of extracting the preparation code from your test and allowing you to focus on the workflow you're testing. You can also use such a fixture across multiple tests, reducing code duplication.
Debugging Tests
If you run your tests headless, you have limited debugging options. However, you can change this locally by running your tests with the --debug option. If you then select only a specific browser with --project, you can easily debug. The --debug option also ensures that Playwright automatically opens its inspector, providing you with additional debugging tools.
Another handy tool is the VS Code extension for Playwright. This tool allows you to run your Playwright tests directly from VS Code. You can then see the test results directly in the development environment, without having to switch to the command line. Another feature offered by this extension is that you can debug your tests directly in the development environment.
You can also have Playwright take a screenshot of your application's state when a test fails. This can sometimes be helpful when trying to track down a bug.
Best Practices for Writing Tests
To ensure your Playwright tests are robust and maintainable, consider the following best practices:
- Utilize stable selectors: Prefer data-testid or getByRole selectors over brittle CSS selectors or dynamic attributes. This approach enhances test resilience against UI changes.
- Leverage auto-waiting: Playwright's auto-waiting capabilities reduce flakiness. Avoid manual timeouts; instead, use built-in assertions like toBeVisible() or toHaveText() that automatically wait for conditions to be met.
- Focus on user behavior: Write tests that mimic real user interactions rather than testing internal implementation details. This approach ensures tests remain relevant as the application evolves.
- Avoid testing third-party integrations: Mock external services to prevent tests from failing due to issues outside your control. This strategy enhances test reliability.
By adhering to these practices, you can create a reliable and efficient test suite that accurately reflects user experiences.
Common Pitfalls and How to Avoid Them
While working with Playwright, be mindful of these common pitfalls:
- Inadequate waiting strategies: Failing to wait for elements to load can lead to flaky tests. Utilize Playwright's auto-waiting features and avoid hard-coded delays.
- Hard-coding test data: Embedding static data within tests reduces flexibility. Instead, externalize test data to allow for easier maintenance and scalability.
- Overlooking test reporting: Without proper reporting, identifying and addressing test failures becomes challenging. Implement comprehensive reporting to monitor test outcomes effectively.
Implementing Playwright effectively lays the foundation for robust end-to-end testing.
Wrapping Up
Implementing Playwright effectively lays the foundation for robust end-to-end testing. To further streamline your testing process, consider leveraging platforms like Autify. Built on Playwright, Autify offers a low-code solution that empowers teams to write tests efficiently while retaining the flexibility of full-code when needed. This approach enables you to accelerate test creation and maintenance, ensuring your applications remain reliable and user-friendly.