Search code examples
typescriptasync-awaitpromiseplaywright

How do I avoid .then or repeated await when chaining promises in TypeScript?


I wrote some code to wrap the Playwright Page to simplify a few operations. In one of my operations, I wanted to do something like typing and tabbing (this simulates the user behaviour better even if it is slower for Playwright so it's primarily to test that tab order works as expected).

The class to wrap would be something like this:

import { Locator, Page } from "@playwright/test";
class SimplePage {
  constructor(private readonly page: Page) {}
  async type(text: string): Promise<this> {
    await this.page.keyboard.type(text);
    return this;
  }
  async tab(): Promise<this> {
    await this.page.keyboard.press("Tab");
    return this;
  }  
}

At present the only way I can use this in my code is as follows:

await myPage.type(programName)
  .then((p) => p.tab())
  .then((p) => p.type(shortName))
  .then((p) => p.tab());

or

await myPage.type(programName);
await myPage.tab();
await myPage.type(shortName);
await myPage.tab();

I was wondering if there's anyway to construct it so the following would work

await myPage
  .type(programName)
  .tab()
  .type(shortName)
  .tab();

Solution

  • You could give your SimplePage class a promise property, and at every call of tab or text, replace that promise with the new chained promise.

    Finally, make SimplePage a thenable, by defining a then method. This way it will respond as expected to an await operator:

    class SimplePage {
      private promise: Promise<void> = Promise.resolve();
    
      constructor(private readonly page: Page) {}
    
      type(text: string): SimplePage {
        this.promise = this.promise.then(() => this.page.keyboard.type(text));
        return this;
      }
    
      tab(): SimplePage {
        this.promise = this.promise.then(() => this.page.keyboard.press("Tab"));
        return this;
      }
    
      then(onFulfill: (value: any) => void, onReject: (error: any) => void) {
        return this.promise.then(onFulfill, onReject);
      }
    }
    

    Implementing PromiseLike:

    The type declaration for then would be:

    class SimplePage implements PromiseLike<void> {
      private promise: Promise<void> = Promise.resolve();
    
      constructor(private readonly page: Page) {}
    
      type(text: string): SimplePage {
        this.promise = this.promise.then(() => this.page.keyboard.type(text));
        return this;
      }
    
      tab(): SimplePage {
        this.promise = this.promise.then(() => this.page.keyboard.press("Tab"));
        return this;
      }
    
      then<TResult1, TResult2>(
          onfulfilled?: ((value: void) => TResult1 | PromiseLike<TResult1>) | null | undefined, 
          onrejected?:  ((reason: any) => TResult2 | PromiseLike<TResult2>) | null | undefined
          ): PromiseLike<TResult1|TResult2> {
        return this.promise.then(onfulfilled, onrejected);
      }
    }