2

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("foo@email.com");
  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 awaiting. 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?

msmith1114
  • 2,717
  • 3
  • 33
  • 84

3 Answers3

3

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("foo@email.com") 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 completed, the execution will resume, regardless of the status of fill("foo@email.com").

This is a high-level walkthrough. I think this could become more "in-depth" and specific, but it explains the flows.

hardkoded
  • 18,915
  • 3
  • 52
  • 64
  • I assume playwright keeps track of 1 "locator" so the original locator is getting overridden? I assumed it maybe kept track of both locators in it's loop. – msmith1114 May 02 '23 at 14:40
  • Locators are declarative. It just says "when you call an action like `.fill()` on this locator, we'll run the query for that locator and then run the action on the most recent element match based on the latest version of the page". Locators aren't actions, they're just selection strategies you can use whenever you want to determine what DOM elements the action will run on. – ggorlen May 02 '23 at 14:54
2

This 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 combination 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.

ggorlen
  • 44,755
  • 7
  • 76
  • 106
  • I guess in the case of `click` it didn't specify it returned a promise (but it does in VSCode) which I assumed it did anyways. I guess my confusion is since Locator is just a constructor.....it presumably would have 2 locators with the correct "element" it was pointing to, even without the await. So why would it fill in both on the same element? (Even the trace/debug log states it finds the correct email/password elements) – msmith1114 May 02 '23 at 16:13
  • You're right about `.click`--maybe you can PR an improvement. I guess if it's `Promise` they omit it. Not awaiting an action on a locator just means you have a race condition and can't do anything with the result. It's usually a mistake, so I'm not sure there's much value in reasoning about what happens when you don't await a promise. The thing that triggers the locator to take action is the action method call, not `await`. – ggorlen May 02 '23 at 16:47
  • I'm not sure what you mean by "So why would it fill in both on the same element?" -- I'd have to see a [mcve] if you're having a problem with a specific page. I'd ask that as a new question, though. – ggorlen May 02 '23 at 16:56
  • I see think I see what you're asking now and updated the answer to address it. – ggorlen May 02 '23 at 23:27
2

There is no such thing as "Playwright Promises", just Promises.

I think its simply a matter of understanding that all playwright locator actions(not locator definitions which return plain objects) return promises and the await keyword is required to wait for the completion of the Promises as usual in any JavaScript code.

And when we combine both(locator definition+ Action in single chained statement) we still need await for eventual action promise resolution.

Vishal Aggarwal
  • 1,929
  • 1
  • 13
  • 23