Testing with Playwright
Learning objectives
- Knows how to write end-to-end tests.
Let's write a set of tests for the task management application built in Application Example I. When considering what to test, one can consider the possible use cases for the application. In our case, there would be at least the following use cases:
- Opening up the page
- Creating a task
- Opening up the task page
To work with these, let's create a new file called task-management.spec.js
, which will contain the tests. As the starting point, let's use the contents from the old hello-world.spec.js
, which are as follows (this is the test from the last that did not work).
const { test, expect } = require("@playwright/test");
test("Server responds with the text 'Hullo world!'", async ({ page }) => {
const response = await page.goto("/");
expect(await response.text()).toBe("Hullo world!");
});
Opening up the page
Let's start by writing a test that opens up the page and verifies that the page title is Task application!
, that the page has a level one heading Tasks
, and that the page has two level two headings which are Add a task
and Active tasks
. For the checks, we will reply on Playwright Assertions. As a starting point, we have a test that goes to the main page using await page.goto("/")
.
const { test, expect } = require("@playwright/test");
test("Main page has expected title and headings.", async ({ page }) => {
await page.goto("/");
});
To verify that the page has a certain title, we use expect(page).toHaveTitle(titleOrRegExp). As want that the title is Task application!
, the assertion is written as expect(page).toHaveTitle("Task application!")
.
const { test, expect } = require("@playwright/test");
test("Main page has expected title and headings.", async ({ page }) => {
await page.goto("/");
await expect(page).toHaveTitle("Task application!");
});
Remember to use await
also for the expectations.
As we are building the tests from the ground up, we can check that they work by running them. When we run the tests with Playwright, we see that the test passes.
✓ 1 [e2e-headless-chrome] › tests/task-management.spec.js:3:1 › Main page has expected title and headings. (415ms)
Let's continue. Next, we need to check the level one heading. For this, we use a family of Selectors that allow us to identify contents from the page. Selectors are used together with a method locator
of the page
element. To look for the h1
element, we write expect(page.locator("h1"))
, which is followed by an expectation from the identified element. In our case, we use expect(locator).toHaveText(expected). As we want that the level one heading is Tasks
, the assertion is written as expect(page.locator("h1")).toHaveText("Tasks")
.
const { test, expect } = require("@playwright/test");
test("Main page has expected title and headings.", async ({ page }) => {
await page.goto("/");
await expect(page).toHaveTitle("Task application!");
await expect(page.locator("h1")).toHaveText("Tasks");
});
With this change, the tests continue to pass.
✓ 1 [e2e-headless-chrome] › tests/task-management.spec.js:3:1 › Main page has expected title and headings. (454ms)
The same approach can be used for identifying the level two heading texts. When a locator finds multiple values, we can check that they match values given in an array. In our case, we wish that the locator of the level two heading returns two values, Add a task
and Active tasks
. This is written as expect(page.locator("h2")).toHaveText(["Add a task", "Active tasks"])
.
const { test, expect } = require("@playwright/test");
test("Main page has expected title and headings.", async ({ page }) => {
await page.goto("/");
await expect(page).toHaveTitle("Task application!");
await expect(page.locator("h1")).toHaveText("Tasks");
await expect(page.locator("h2")).toHaveText(["Add a task", "Active tasks"]);
});
Again, the tests continue to pass.
✓ 1 [e2e-headless-chrome] › tests/task-management.spec.js:3:1 › Main page has expected title and headings. (494ms)
Note that the results from a locator are returned in the same order that the page has them. As an example, the following test would fail as the level two headings are not in the same order as the tests expect them to be.
const { test, expect } = require("@playwright/test");
test("Main page has expected title and headings.", async ({ page }) => {
await page.goto("/");
await expect(page).toHaveTitle("Task application!");
await expect(page.locator("h1")).toHaveText("Tasks");
await expect(page.locator("h2")).toHaveText(["Active tasks", "Add a task"]);
});
Creating a task
Let's continue with creating a task. We continue by writing a new test to the previous file, starting again by entering the page.
const { test, expect } = require("@playwright/test");
test("Main page has expected title and headings.", async ({ page }) => {
await page.goto("/");
await expect(page).toHaveTitle("Task application!");
await expect(page.locator("h1")).toHaveText("Tasks");
await expect(page.locator("h2")).toHaveText(["Active tasks", "Add a task"]);
});
test("Can create a task.", async ({ page }) => {
await page.goto("/");
});
To create a task, we need to type in the name of a task and press the button used to submit the form. As both the text field and the button are of type input
, we can provide additional details to the selector to determine what type of an input we wish to look for. As an example, to find an input with the type text
, we would use page.locator("input[type=text]")
, while to find an input with the type submit
, we would use page.locator("input[type=submit]")
. To type characters, we use the type method of the locator
, while to click on an element, we use click method.
Combined, the test looks as follows.
test("Can create a task.", async ({ page }) => {
await page.goto("/");
await page.locator("input[type=text]").type("My cool new task");
await page.locator("input[type=submit]").click();
});
But! Does the test actually verify that things went as expected? No, it does not. It enters the page, types in content, and presses a button. These steps have to be possible for the test to pass. To verify that the content indeed is added, we wish to verify that the newly added task indeed appears on the page.
In the task management application, tasks are listed using an unordered list that contains links. The links are created using the a
element. A first version for the test could use a locator to find an a
element with the typed in text, similar to what we used when testing that the headings had certain content. This would be done using expect(page.locator("a")).toHaveText("My cool new task")
.
test("Can create a task.", async ({ page }) => {
await page.goto("/");
await page.locator("input[type=text]").type("My cool new task");
await page.locator("input[type=submit]").click();
await expect(page.locator("a")).toHaveText("My cool new task");
});
On a first glance, this seems to work nicely. There is a caveat though -- if there are multiple a
elements, the test will fail.
One possibility would be to take a stand that newly created tasks always have to appear at the top of the list, and then check the text of the first link on the page. Another possibility would be to restrain the application so that the task names have to be unique, and adjust the way the selector works to match a certain a
element with the given text. Both of these would require modifying the application though.
Yet another possibility is to add a small degree of randomness to the typed in task name, and look for that particular task name with a selector. Using locator, we can look for link elements that have a certain text as follows: page.locator("a >> text='a certain text'")
. Randomness, on the other hand, can be added with e.g. the Math.random()
function.
Jointly, these would work as follows.
test("Can create a task.", async ({ page }) => {
await page.goto("/");
const taskName = `My task: ${Math.random()}`;
await page.locator("input[type=text]").type(taskName);
await page.locator("input[type=submit]").click();
await expect(page.locator(`a >> text='${taskName}'`)).toHaveText(taskName);
});
When we run the tests, we notice that both tests pass.
✓ 1 [e2e-headless-chrome] › tests/task-management.spec.js:3:1 › Main page has expected title and headings. (553ms)
✓ 2 [e2e-headless-chrome] › tests/task-management.spec.js:10:1 › Can create a task. (425ms)
Opening up the task page
Let's continue with opening up the page of a specific task. Again, we start by writing a new test, where we enter the page. To check that a page for a task is created, we first create a task, click on the task, and check that we are on a page for the task. The task page should contain a level one heading with the name of the task. To achieve this, we can use what we have already learned -- the following example outlines the full test.
test("Can open a task page.", async ({ page }) => {
await page.goto("/");
const taskName = `My task: ${Math.random()}`;
await page.locator("input[type=text]").type(taskName);
await page.locator("input[type=submit]").click();
await page.locator(`a >> text='${taskName}'`).click();
await expect(page.locator("h1")).toHaveText(taskName);
});
Now, when we run the tests again, we notice that all the tests pass.
✓ 1 [e2e-headless-chrome] › tests/task-management.spec.js:3:1 › Main page has expected title and headings. (461ms)
✓ 2 [e2e-headless-chrome] › tests/task-management.spec.js:10:1 › Can create a task. (496ms)
✓ 3 [e2e-headless-chrome] › tests/task-management.spec.js:18:1 › Can open a task page. (473ms)
Next, we would create a test for adding a work entry on the task page, and continue from there.
Easier testing with Selectors
The selectors behave in a similar way to using CSS, where we used classes and identifiers to identify content. To make testing easier, it can be at times meaningful to add identifiers to HTML elements of the page, which allows direct access of those elements. For example, we would add a property id
to the text field that is used to input a task, which could then be directly accessed with a selector. Check out Playwright documentation for more details.