So im trying to figure out/explain why a piece of code works the way it does. Im familiar with async for the most part and understand how promises work on a basic level, but I am trying to explain why a block of code works the way it does in Playwright.
import { test, expect } from "@playwright/test";
test.only("Basic Login", async ({ page }) => {
//Basic test that demonstrates logging into a webpage
await page.goto("/");
page
.getByPlaceholder("Email Address")
.fill("[email protected]");
await page.getByPlaceholder("Password").fill("bar");
await page.getByRole("button", { name: "Sign In" }).click();
await expect(page).toHaveTitle("The Foobar Page");
});
So obviously this will not work, because the first "fill" for email address input isn't await
ing. Currently the behavior is it fills in the password field with both the email and password strings together (I assume, I can't see it because it's a password field, but the number of *
's is longer so I assume it's putting both fill
strings into the password field.
However I can't really understand why. getByPlaceholder
will keep retrying until it fails. and I also believe fill
waits for checks (IE: the field is fillable/visible/etc...)
However Playwright specifically doesn't state whether these return promises specifically. I ASSUME they do but the docs don't specifically say.
So what is actually going on here step by step? I can't really "walk" my way through it, so it makes it difficult to explain to others. As in I understand why it doesn't work on a high level, but not exactly why it doesn't work in detail.
This also leads me to a side question: Does Playwright wait for BOTH promises when chaining functions? Or only the last one?
So im trying to figure out/explain why a piece of code works the way it does. Im familiar with async for the most part and understand how promises work on a basic level, but I am trying to explain why a block of code works the way it does in Playwright.
import { test, expect } from "@playwright/test";
test.only("Basic Login", async ({ page }) => {
//Basic test that demonstrates logging into a webpage
await page.goto("/");
page
.getByPlaceholder("Email Address")
.fill("[email protected]");
await page.getByPlaceholder("Password").fill("bar");
await page.getByRole("button", { name: "Sign In" }).click();
await expect(page).toHaveTitle("The Foobar Page");
});
So obviously this will not work, because the first "fill" for email address input isn't await
ing. Currently the behavior is it fills in the password field with both the email and password strings together (I assume, I can't see it because it's a password field, but the number of *
's is longer so I assume it's putting both fill
strings into the password field.
However I can't really understand why. getByPlaceholder
will keep retrying until it fails. and I also believe fill
waits for checks (IE: the field is fillable/visible/etc...)
However Playwright specifically doesn't state whether these return promises specifically. I ASSUME they do but the docs don't specifically say.
So what is actually going on here step by step? I can't really "walk" my way through it, so it makes it difficult to explain to others. As in I understand why it doesn't work on a high level, but not exactly why it doesn't work in detail.
This also leads me to a side question: Does Playwright wait for BOTH promises when chaining functions? Or only the last one?
Share Improve this question edited Jun 24, 2024 at 15:51 ggorlen 57.1k8 gold badges110 silver badges150 bronze badges asked May 2, 2023 at 12:53 msmith1114msmith1114 3,2318 gold badges47 silver badges104 bronze badges 3- on side note you look like someone from cypress world :) – Vishal Aggarwal Commented May 2, 2023 at 15:40
- @VishalAggarwal Yup I definitely am more familiar with Cypress but moving over to Playwright. – msmith1114 Commented May 2, 2023 at 16:10
- why did you move? – Ooker Commented Nov 21, 2024 at 13:36
3 Answers
Reset to default 6This also leads me to a side question: Does Playwright wait for BOTH promises when chaining functions? Or only the last one?
Locators don't return promises, only the "action" methods like .fill()
, .click()
, .evaluate()
and so forth return promises.
Think of locators as constructors in normal JS:
const bike = new Bicycle(); // locator declaration
await bike.ride(); // asynchronous action
You could write this in a chained style like:
await new Bicycle().ride();
new Bike
isn't async but .ride()
is. The difference between the contrived example above and Playwright is that locators don't use new Locator
syntax:
await bicycle().ride();
// or with an intermediate variable:
const bike = bicycle();
await bike.ride();
// or with some declarative chained options, like Playwright's .filter:
await bicycle()
.withRedPaint()
.withFatTires()
.ride(); // .ride() is the only method that returns
// a promise; the rest return `this`
I can't understand why
getByPlaceholder
will keep retrying until it fails.
That's the way the library is designed. There's a retry loop in Playwright's code for locator selections because waiting is normally what you want to do.
When you create a locator, Playwright doesn't take action on the page. You've simply created a declarative blueprint, a strategy for how to select something, but haven't acted upon that strategy yet.
When you call an action like .click()
on a locator, Playwright springs into action, looks at the locator parameter string and options and goes to the page you're automating to find the elements. It retries until it finds something, then runs the action on it. You are now executing the selector/action plan you declared with the bination of the locator and a method like .fill()
.
However Playwright specifically doesn't state whether these return promises specifically. I ASSUME they do but the docs don't specifically say.
This could be improved in Playwright's docs. The docs use Promise<SomeType>
when a method returns a promise that resolves to a value, but seem to omit Promise<void>
for asynchronous methods such as .click
that need to be awaited but don't resolve the promise with a value, which is confusing.
Currently the behavior is it fills in the password field with both the email and password strings together.
Without await
, you introduce concurrency and Playwright tries to fill two fields at once. Filling a field involves focusing on it, so it's likely that whichever .fill()
happens second (the password .fill()
, usually) will cause the first .fill()
's focus to be lost and both actions fight over the same input. But when there's a race condition, it's almost certainly a bug, so I'd just be sure to await
everything and don't spend too much energy trying to reason about what might happen when you neglect to.
In contrast to Playwright, Puppeteer uses a more imperative style (at the time of writing) and does not have locators*. Playwright has a number of Puppeteer-based legacy methods that are more imperative and are now discouraged or deprecated. If you prefer imperative code, you might want to use Puppeteer.
If you're uncertain about why Playwright and Puppeteer APIs are asynchronous in the first place, see this post.
*: As of August 2023, Puppeteer added experimental support for locators.
getByPlaceholder will keep retrying until it fails.
Locators are not async. They just return a locator. The fill
function will be the one retrying.
I think that also answers "This also leads me to a side question: Does Playwright wait for BOTH promises when chaining functions? Or only the last one?".
So what is actually going on here step by step? I can't really "walk" my way through it, so it makes it difficult to explain to others. As in I understand why it doesn't work on a high level, but not exactly why it doesn't work in detail.
It's all about the event loop:
getByPlaceholder("Email Address")
will just return a locator.fill("[email protected]")
will be the first one performing an async task. Let's say it looks for the element matching the locator.- Once the Promise is created, the execution will move on.
getByPlaceholder("Password")
will return a locator (sync action)fill("bar")
will initiate another async call.- From there, Node will assign execution time to both Promises.
- Once
fill("bar")
is pleted, the execution will resume, regardless of the status offill("[email protected]")
.
This is a high-level walkthrough. I think this could bee more "in-depth" and specific, but it explains the flows.
There is no such thing as "Playwright Promises", just Promises.
It's simply a matter of understanding that Playwright locators are lazily evaluated and only return promises on performing actions.
Hence the await
keyword is required to wait for the pletion of the Promises as usual in any JavaScript code.
And when we bine both locator definition and action in a single, chained statement, we still need await for eventual action promise resolution.