3

Currently I am writing a small test project for which I use the Page Object Model and Playwright. I have pages with methods returning either the page itself or another page. This makes it easier for me to write the tests. However, as I am using async methods I get a lot of await.

var homePage = new HomePage(page);
await (await (await (await homePage.goto())
.DoSomething())
.NavigateToNextPage())
.ValidateResult();

I am wondering whether there is a way to avoid queueing al the awaits.Or should shouldn't I use async by default as my tests are rather sequental by nature.

I found Promise.all() however thats doesn't seem to be working in this case as I am chaining the methods.

Simon
  • 61
  • 1
  • 5

1 Answers1

1

I'd remove chaining from the API and break the calls onto multiple lines:

const homePage = new HomePage(page);
await homePage.goto();
await homePage.doSomething();
await homePage.navigateToNextPage();
await homePage.validateResult();

To do a Playwright locator-style chain only works when you have a series of calls that are lazily evaluated and end with some sort of action. To use an example from the Playwright docs:

await page
    .getByRole('listitem')
    .filter({ hasText: /Product 2/ })
    .getByRole('button', { name: 'Add to cart' })
    .click();

Here, the only action is click(). Everything else can be chained before the action because the purpose of these methods is to select and filter in preparation for the click. A page instance can return synchronously-chainable objects up until that final click() which returns a promise that evaluates the chain and disallows further chaining.

But in your case, each line appears to be an action that should be able to be executed on its own, and doesn't involve any optional preparatory steps. Playwright doesn't support chaining on actions, like

await page.goto().click().waitForNavigation().click();

If the pattern I've shown at the top of this answer looks too "boring", consider that Puppeteer's API (and most of Playwright, other than locators) is 100% in this style. It's normal and unsurprising to read, write and use. One can argue that Playwright's lazy locators are more clever and therefore harder to reason about than Puppeteer's non-chained API.

But if you really want to do this, the easiest way is to pick a function to act as the action, and use that to flush the chain:

class HomePage {
  constructor(page) {
    this.page = page;
    this.actions = [];
  }

  goto() {
    this.actions.push(this.page.goto());
    return this;
  }

  doSomething() {
    this.actions.push(
      this.page.doSomething(),
      this.page.doSomethingElse(),
      this.page.doAnotherSubStep(),
      this.page.click()
    );
    return this;
  }

  navigateToNextPage() {
    this.actions.push(this.page.navigateToNextPage());
    return this;
  }

  validateResult() {
    this.actions.push(this.page.validateResult());
    return this;
  }

  async exec() {
    for (const action of this.actions) {
      await action;
    }

    this.actions = [];
  }
}

const homePage = new HomePage(page);
await homePage
  .goto()
  .doSomething()
  .navigateToNextPage()
  .validateResult()
  .exec();

Or you can make validateResult flush out the promises instead of exec, depending on your use case.

The problem with this design is that if you just want to take a single action, it's awkward. If you use exec() to flush, you'll have to call exec() (or some other set of action methods) after every action, and that's superfluous code. You could use a different set of chainable methods, which is prohibitively confusing for the caller and callee.

If you use validateResult() to flush and you're sure you'll always be calling that at the end, maybe it's doable. Then the decision is whether my first proposal is more or less confusing (both to maintain and use) than the chained version.

Since you're dealing with what looks like a unit of atomic work, it may be best to abstract the whole chain into a single function, validateHomePage() or similar.

Note that I'm using camelCase rather than PascalCase and const rather than var.

ggorlen
  • 44,755
  • 7
  • 76
  • 106
  • Thanks a lot for your detailed answer. I think i am going to remove the chaining, prefer to keep it simple. What i created works but with longer chains it just gets really messy with all the awaits. – Simon Apr 04 '23 at 18:21
  • Looks like there's a library for this: https://github.com/hdorgeval/playwright-fluent. I haven't triied it. – ggorlen Apr 22 '23 at 02:35