Search code examples
typescriptplaywrightplaywright-test

Is it possible to write a "isAlertDisplayed" method on a separate AlertHelper file?


I am trying to write a "isAlertDisplayed" method on a separate AlertHelper file using Playwright TypeScript. My test scenario is when a button is clicked for 5 seconds there is no alert.

It is the test of "On button click, alert will appear after 5 seconds" at link. when you click the button alert doesn't appear for 5 seconds. I want to verify that:

  • the alert is not there at the fourth second
  • the alert there after the fifth second. I couldn't find proper examples to do it on the internet. and asked even ChatGPT but the solution shared is also not working.

My page parameter is passed through a PageManager instance and Page Object instances are created and returned by PageManager object pm.

Here is how I call my method:

// Verifying alert is not displayed after 4 seconds
const isAlertPresent1 = await pm.AlertHelper().isAlertDisplayed(4000);
expect(isAlertPresent1).toBe(false);

// Verifying alert is displayed after waiting for another 2 seconds
const isAlertPresent2 = await pm.AlertHelper().isAlertDisplayed(2000,true);
expect(isAlertPresent2).toBe(true);

And the method provided by ChatGPT which does not work is as follows:

async isAlertDisplayed(timeout: number = 1000, dismissAlert: boolean = true): Promise<boolean> {
    try {
        // Add an event listener for the dialog event
        const alertPromise = new Promise<boolean>(resolve => {
            this.page.once('dialog', async dialog => {
                if (dismissAlert) {
                    await dialog.dismiss(); // Dismiss the alert to allow further actions if needed
                }
                resolve(true);
            });
        });

        // Wait for the specified timeout period
        await new Promise(resolve => setTimeout(resolve, timeout));

        // Check if the alert was displayed during the timeout period
        return await Promise.race([alertPromise, Promise.resolve(false)]);
    } catch (error) {
        console.error('Error checking for alert:', error);
        return false;
    }
}

Any idea how to implement such method? It is a very simple function browser.isAlertOpen() in WebdriverIO but very difficult to write in Playwright. Couldn't find any sample code or methods on internet.


Solution

  • ChatGPT has the right idea but is missing a critical step: removing the page.on("dialog" listener before resolving the promise. If the listener is not removed, then it leaks and the first call's handler intercepts the dialog even though it already rejected, causing the second call to throw Error: dialog.dismiss: Cannot dismiss dialog which is already handled!.

    The solution is to remove the listener if the timeout happens before the dialog:

    import {expect, test} from "@playwright/test"; // ^1.42.1
    
    const waitForDialog = async (page, timeout): Promise<boolean> => {
      return new Promise((resolve, reject) => {
        const handleDialog = async dialog => {
          await dialog.dismiss();
          resolve(true);
        };
        page.once("dialog", handleDialog);
        setTimeout(() => {
          page.off("dialog", handleDialog);
          resolve(false);
        }, timeout);
      });
    };
    
    test("clicking button shows alert after 5 seconds", async ({page}) => {
      test.setTimeout(10_000)
      const url = "https://demoqa.com/alerts";
      await page.goto(url, {waitUntil: "domcontentloaded"});
      const dialogPromise = waitForDialog(page, 4000);
      await page.locator("#timerAlertButton").click();
      expect(await dialogPromise).toBe(false);
      expect(await waitForDialog(page, 2000)).toBe(true);
    });
    

    Comment out page.off to reproduce the bug.

    You can modify this to be a single call, something like waitForDialog(page, {begin: 4000, end: 6000}): throw or reject with false if an alert goes off before begin, as well as if end is exceeded. And/or make it a custom matcher.

    Note that because web page timing is a fickle thing, you might want to give a bit more leeway for the dialog than 2 seconds.

    Maybe someday there'll be a promise-based dialog handler in Playwright like waitForDialog. It'll be easier to do this then.