Search code examples
node.jsloopseventsdialogpuppeteer

How to use page.on("dialog" in a loop with Puppeteer?


As it is stated in the documentation of Puppeteer, the basic usage of "dialog" event is the following:

page.on('dialog', async (dialog) => {
  await dialog.dismiss() 
  // or await dialog.accept()
 })

I want to loop through a list of URLs each of them firing a confirm dialog. But I want to accept or dismiss the dialog depending of the page content.

I wonder if it is possible?

When I use it in a loop I get an error: "Cannot dismiss dialog which is already handled!"

for (let url in urls) {
  if (condition) {
    page.on("dialog", async (dialog) => {
      await dialog.accept();
    });
  } else {
    page.on("dialog", async (dialog) => {
      await dialog.dismiss();
    });
  }
}

I'm adding a listener on every loop, so I'm getting an error.

But when I move the "dialog" listener out of the loop, I get a "dialog is undefined" error.

 page.on("dialog", async (dialog) => {

    for (let url in urls) {
      if (condition) {
        await dialog.accept();
      } else {
        await dialog.dismiss();
      }
    }
});

I tried to make a custom event listener.

await page.exposeFunction("test", async (e) => {
  // But I don't know how to dismiss or accept the confirm dialog here.
});

await page.evaluate(() => {
  window.addEventListener("confirm", window.test());
});

The problem with this approach is that I don't have access to handleJavaScriptDialog which is responsible for handling the confirm dialog returns: https://pub.dev/documentation/puppeteer/latest/puppeteer/Dialog/dismiss.html

So far I think the only solution I have is to emulate Enter key press to accept the confirm dialog, or to just go to the next page when I want to dismiss the confirm dialog.

Are there any solutions to using dialog events in a loop like this with Puppeteer?

======

Update

======

//Example for @ggorlen

for (let url in urls) {
  await page.goto(url);

  const dialogDismissed = new Promise((resolve, reject) => {
    const handler = async (dialog) => {
      await dialog.dismiss();
      resolve(dialog.message());
    };
    page.on("dialog", handler);
  });

  const dialogAccepted = new Promise((resolve, reject) => {
    const handler = async (dialog) => {
      await dialog.accept();
      resolve(dialog.message());
    };
    page.on("dialog", handler);
  });

  await page.evaluate(() => window.confirm("Yes or No?"));

  if (condition) {
    //want to accept
    //how to handle the dialog promise here?
  } else {
    //want to dismiss
    //how to handle the dialog promise here?
  }
}

======

Update 2

======

//Based on @ggorlen answer but without promisifing the handler

const puppeteer = require("puppeteer");

let browser;
(async () => {
  const html = `<html><body><script>
    document.write(confirm("yes or no?") ? "confirmed" : "rejected");
  </script></body></html>`;
  browser = await puppeteer.launch({
    headless: true,
  });
  const [page] = await browser.pages();
  const urls = ["just", "a", "demo", "replace", "this"];

  for (const url of urls) {
    const someCondition = Math.random() < 0.5; // for example

    //This bloc is in question.
    //Is there a need to promisify?
    page.once("dialog", async (dialog) => {
      console.log(dialog.message());
      await (someCondition ? dialog.accept() : dialog.dismiss());
    });

    //await page.goto(url, {waitUntil: "networkidle0"});
    await page.setContent(html);
    console.log(await page.$eval("body", (el) => el.innerText));
  }
})()
  .catch((err) => console.error(err))
  .finally(() => browser?.close());

Solution

  • This answer is a variant of Puppeteer not picking up dialog box. A quick summary of that answer: .on handlers can be promisified to make it easy to integrate waiting for them into control flow without a mess of callbacks. An important nuance that seems lost in OP's code is that if you're only waiting once, use .once rather than .on, or use .off to remove the listener. After it's been resolved, the listener becomes stale.

    In this case, let's say you have a bunch of URLs to pages that show confirmation dialogs (or you inject your own confirmation dialog), and for each URL, you want to add a handler for the dialog that lets you accept or dismiss it based on a condition. You might also want to collect the message from the dialog, which is shown below.

    Here's a simple example of this:

    const puppeteer = require("puppeteer"); // ^21.4.1
    
    const html = `<html><body><script>
      document.write(confirm("yes or no?") ? "confirmed" : "rejected");
    </script></body></html>`;
    
    let browser;
    (async () => {
      browser = await puppeteer.launch({headless: true});
      const [page] = await browser.pages();
      const urls = ["just", "a", "demo", "replace", "this"];
    
      for (const url of urls) {
        const someCondition = Math.random() < 0.5; // for example
    
        const dialogHandled = new Promise((resolve, reject) => {
          const handler = async dialog => {
            await (someCondition ? dialog.accept() : dialog.dismiss());
            resolve(dialog.message());
          };
          page.once("dialog", handler);
        });
    
        //await page.goto(url, {waitUntil: "networkidle0"});
        await page.setContent(html); // for demonstration
        const msg = await dialogHandled;
        console.log(msg, await page.$eval("body", el => el.innerText));
      }
    })()
      .catch(err => console.error(err))
      .finally(() => browser?.close());
    

    A sample run looks something like:

    yes or no? confirmed
    yes or no? confirmed
    yes or no? confirmed
    yes or no? rejected
    yes or no? rejected