javascriptasync-awaitplaywright

Understanding Playwright locator Promises


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?


Solution

  • 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.